This article documents and describes a solution for automated web testing using Puppeteer and relying on conventions to avoid repeated changes to test cases. It mainly solves the run-time problem that the implicated error caused by the modification of the code cannot be found when there are many pages. The article was first published
Personal blog

The cause of

We are currently working on a project with tens of pages and 100,000 + lines of code, and as products change, there are always problems like this. When some business logic or functionality is added or modified (especially common logic), the common logic or components often involve problems elsewhere. Because of the tester limitations, it is difficult to complete a module unit and retest all functionality. At the same time, due to differences in environment and data (as well as negligence of code integrity during development), code may have problems with parsing and presenting specific data that are difficult to detect during development and testing. In general, we hope to have such a tool to help us solve the above problems:

  1. After code and function changes, the system can automatically access the page of each function to detect problems
  2. For a large amount of data content, batch access to detect whether there are problems in the presentation of different data
  3. Test and code functions should be decoupled as far as possible, so as to avoid the need to modify test cases every time new functions are added, and maintenance costs are too high
  4. Regular test tasks to timely discover the completeness of the data platform for the presentation of new data

Among them, the most important problem is to decouple the test code from the function, avoiding the need to add new test cases with each iteration and modification. How do we do that? First, let’s sort out the functions of the test platform.

Feature set

Since our platform is mainly for data display, we mainly focus on daily display data in the test process, and do not deal with some complex form operations. In view of the above problems, the functions of the automated test tool are as follows:

  1. Visit each page in turn
  2. Access specific content of each page, such as time switch, TAB switch, page switch, table row expansion, etc
  3. For the details links in the data table, select the first 100 to access and drill down to continue testing the page
  4. Catch the error request on the page
  5. Error information is captured, counted and reported

Based on the above review, we can divide the entire application into several test units

  • Page unit to detect the stability of page access of each function
  • Detail page unit. According to the data list of the page, it can skip the detail page in batches and test the stability of the detail page under different parameters
  • Function unit, used to detect page and detail page display types after clicking switch error

By doing this, we write use cases with specific test logic for each unit, thus avoiding frequent changes to the test cases as new features and pages are added.

Puppeteer

With that in mind, let’s take a look at whether Puppeteer’s capabilities and features meet our requirements.

The document address

Puppeteer is a Node library that provides a high-level API for controlling Chromium or Chrome via the DevTools protocol. Puppeteer runs in headless mode by default, but can be run in headless mode by modifying the configuration file.

Puppeteer can be used to:

  • Visit the page and take a screenshot
  • Automatic keyboard input and form submission
  • Simulate click and other user actions
  • And so on and so on.

Let’s introduce their basic functions through some small cases:

Visit a website with ba certification

Puppeteer can create page instances and access the pages using goto methods, which contain a series of methods for performing various operations on the page.

(async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); // Authenticate ({username, password}); // Access page await page.goto('https://example.com'); // Screenshot await page.screenshot({path: 'example.png'}); await browser.close(); }) ();Copy the code

Visit the login page and log in

First, with SPA(single-page application), we all know that the client code does the rendering work only after the page is entered. We need to wait until the page content is rendered. We have the following methods to use

waitUntil

Puppeteer provides the waitUntil parameter for page access, switching, etc., to determine what conditions are met before a page jump is considered complete. Including the following events:

  • Load – When the page load event is triggered
  • Domcontentloaded – When the page’s domContentLoaded event is triggered
  • Networkidle0 – Triggered when there is no more network connection (after at least 500 ms)
  • Networkidle2 – Triggered when there are only 2 network connections (at least 500 ms later)

With waitUnitl, we can confirm that the page has been accessed when all page requests have been completed.

waitFor

The waitFor method can resolve only after the specified action is complete

// wait for selector await page.waitFor('.foo'); // wait for 1 second await page.waitFor(1000); // wait for predicate await page.waitFor(() => !! document.querySelector('.foo'));Copy the code

We can use the waitForSelector method and wait until the login box is successfully rendered

// waitFor the password input box to render await page.waitfor ('#password'); // enter username await page.type('input#username', "username"); // enter password await page.type('input#password', "testpass"); All ([page.waitfornavigation (), resolve page.click('button.login-button'), // Clicking this link will indirectly lead to navigation [); Await page.waitfor (2000) // Get cookies const cookies = await page.cookies()Copy the code

Batch access for links in list contents

It takes advantage of the selector functionality of the Page instance

const table = await page.$('.table') const links = await table.? eval('a.link-detail', links => links.map(link => link.href) ); // Loop access links...Copy the code

Perform error and access listening

Puppeteer listens for errors, requests, etc., so that we can catch and report errors, which is a basic function for testing

Emitted when an exception occurs that the page's JS code did not catch. Page.on (' pagError ', () => {}) // Triggered when the page crashes. Page. On ('error', () => {}) // Triggered when the page sends a request page. On ('request') // Triggered when a response is received for a request page. page.on('response')Copy the code

Through the above small examples, we have found that Puppeteer is very powerful and can fully meet our above requirements for automatic page access. Next, we write a unit use case for our test unit

The final function

From our test unit planning above, we can plan our test path

Visit the website -> Login -> Visit page 1 -> Perform Basic unit tests -> Get details page jump link -> Visit Details page in sequence -> Perform Basic unit tests

-> Visit page 2…

So, we can split up into a few categories, and a few test units, to do each test

Class PageDetal extends Base {} class PageDetal extends Base {} class PageDetal extends Base {} Class Page extends PageDetal {} Class Page extends PageDetal {} extends Page extends Base {}Copy the code

At the same time, how can we track the changes of the test when the function page changes? We can add a custom label test-role for the function we test, and write the test logic according to the custom label during the test.

For example, for the time switch unit, let’s make a simple introduction:

Const timeSwitch = await page.$('[test-role="time-switch"]'); // If the page does not have timeSwitch, do not test if (! TimeSwitch) return // 2. Time switch const buttons = timeSwitch.? ('. Time-switch-button ') // 3. Loop for (let I = 0; i < buttons.length; I ++) {const button = buttons[I] // await button.click() Waiting for the corresponding content to appear, Try {await page.waitfor ('[test-role="time-switch-content"]')} catch (error) {reportError (error)} // screenshot await page.screenshot() }Copy the code

We can write our respective test logic according to our use-case unit. In daily development, we only need to add the corresponding test-role to the content that needs to be tested.

conclusion

According to the above functional division, we well divided the whole application into various test units for unit testing. It is important to note that we are currently only testing the accessibility of the page, only verifying whether the page will fail when the user performs various operations and accesses individual page units. Instead of testing the presentation of the page, it is coupled to the functional content of the page, requiring the writing of separate test cases.