Search:

Load Test Game-Changer: k6 Browser

k6 is one of the first performance testing programs to enable both protocol and browser testing.

Load Test Game-Changer: k6 Browser

You are reading the fourth post in the performance testing series. In case you missed the previous post, here they are:

  1. A Beginner’s Guide to Performance Testing
  2. New Era in Performance Testing: k6
  3. k6 Report Guideline: Understanding Metrics and Output Types

k6 is one of the first performance testing programs to enable both protocol and browser testing. You may now execute your web application's performance test in the web browser, just like any other user. It is a game-changer in the load testing arena.

This functionality allows for simulating real user scenarios more accurately, capturing performance metrics, and analysing web application behaviour comprehensively.

Why should you use this feature?

Using this feature, you can receive browser-specific metrics like total page load time. It makes sure that all elements are interactive, checks for loading spinners that take a long time to disappear, and monitors how the front end responds to thousands of simultaneous protocol-level requests.

ℹTake notice! This functionality is currently in the experimental stage, and k6 says it is still working to make the module stable. To use this feature, make sure you are using the latest k6 version and have installed a Chromium-based browser. Knowing this, let's move on to exploration.

A simple browser test

Before anything else, create a new js file and import the browser module. In the options topic, the executor and browser type are mandatory, and the browser type must be set to 'chromium'. You can select a variety of executors; for the first example, I used 'shared-iterations'.


   const page = browser.newPage();

If you want to change the size of the browser window, you can add the following piece of code.


page.setViewportSize({
       width: 1425,
       height: 1818
   });

After getting the page, you can interact with it. In this example, I visit a url, take a screenshot of the page, and close the page.


 export default async function(){

   const page = browser.newPage();

   page.setViewportSize({
       width: 1425,
       height: 1818
   });

   await page.goto('https://hotel.testplanisphere.dev/en-US/login.html');
   page.screenshot({path: 'screenshot.png'});
   page.close();
}

Let's run the script with this command: k6 run script.js You didn’t see the browser, did you? k6 has default arguments when launching the browser and the headless default value is true. If you want to change this you can use this command:


K6_BROWSER_HEADLESS=false k6 run script.js

It’s time to improve the script to interact with elements on the page such as click, select, and type. The browser module supports CSS and XPath selectors.


 page.locator("input[name='email']").type('clark@example.com');
 page.locator("#password").type('pasword');
 page.locator('#login-button').click();

Pass the selector of the element you want to find on the page to page.locator(). page.locator() will create and return a locator object, which you can later use to interact with the element. Example of the above, it finds the email element and writes ‘clark@example.com’, finds the password element and writes ‘password’ and clicks the login button. You can also write these scripts using the syntax below. The functionality is the same, but I find the markup simpler and more reusable.


 const emailTextBox = page.locator("input[name='email']");
  emailTextBox.type("clark@example.com");
 const passwordTextBox = page.locator("#password");
  passwordTextBox.type("pasword");
 const loginButton = page.locator('#login-button');
  loginButton.click();

Let’s complete the script by adding some validations. k6 provides an assertion structure with 'check' that is similar to other framework assertions. Unlike others, failed checks do not cause the test to abort or end with a failed status. k6 keeps track of the number of failed checks as the test runs.


   check(page, {
       'login is successful':
       page.locator("#logout-form").isVisible() === true,
       'page title is correct':
       page.title().includes('MyPage')
   })

After running the script, you can see the results of the logout element being visible on the page and the assertions about the page title among its outputs.


         /\      |‾‾| /‾‾/   /‾‾/   
     /\  /  \     |  |/  /   /  /    
    /  \/    \    |     (   /   ‾‾\  
   /          \   |  |\  \ |  (‾)  | 
  / __________ \  |__| \__\ \_____/ .io

  execution: local
     script: script.js
     output: -

  scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop):
           * ui: 1 iterations shared among 1 VUs (maxDuration: 10m0s, gracefulStop: 30s)

     ✓ login is successful
     ✓ page title is correct

If the checker gets an error, you can see it in the terminal as follows.


         /\      |‾‾| /‾‾/   /‾‾/   
     /\  /  \     |  |/  /   /  /    
    /  \/    \    |     (   /   ‾‾\  
   /          \   |  |\  \ |  (‾)  | 
  / __________ \  |__| \__\ \_____/ .io

     execution: local
        script: scripts/browserTest.js
        output: -

     scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop):
              * ui: 1 iterations shared among 1 VUs (maxDuration: 10m0s, gracefulStop: 30s)


     ✓ login is successful
     ✗ page title is correct
      ↳  0% -- ✓ 0 / ✗ 1

Combine tests with API and Browser

You can run both browser-level and protocol-level tests in a single script by using the same steps. In this way, you can track the condition of your APIs under load and learn about how the front end performs.


import { browser } from 'k6/experimental/browser';
import { check } from 'k6';
import http from 'k6/http';

export const options = {
 scenarios: {
   browser: {
     executor: 'constant-vus',
     exec: 'browserTest',
     vus: 3,
     duration: '10s',
     options: {
       browser: {
         type: 'chromium',
       },
     },
   },
   api: {
     executor: 'constant-vus',
     exec: 'api',
     vus: 20,
     duration: '1m',
   },
 },
};

export async function browserTest() {
 const page = browser.newPage();

 try {
   await page.goto('https://test.k6.io/browser.php');

   page.locator('#checkbox1').check();

   check(page, {
     'checkbox is checked':
       page.locator('#checkbox-info-display').textContent() === 'Thanks for checking the box',
   });
 } finally {
   page.close();
 }
}

export function api() {
 const res = http.get('https://test.k6.io/news.php');

 check(res, {
   'status is 200': (r) => r.status === 200,
 });
}


Understanding the browser Metrics

In the previous blog post, I examined the metrics in detail. Now, I will explain the metrics specific to the browser.


     browser_data_received.......: 712 kB  281 kB/s
     browser_data_sent...........: 4.4 kB  1.7 kB/s
     browser_http_req_duration...: avg=46ms     min=4.01ms   med=26.11ms  max=138.57ms p(90)=129.53ms p(95)=134.05ms
     browser_http_req_failed.....: 9.09%   ✓ 1        ✗ 10 
     browser_web_vital_cls.......: avg=0.042967 min=0.042967 med=0.042967 max=0.042967 p(90)=0.042967 p(95)=0.042967
     browser_web_vital_fcp.......: avg=188.75ms min=168.8ms  med=188.75ms max=208.7ms  p(90)=204.71ms p(95)=206.7ms 
     browser_web_vital_fid.......: avg=1.7ms    min=1.7ms    med=1.7ms    max=1.7ms    p(90)=1.7ms    p(95)=1.7ms   
     browser_web_vital_lcp.......: avg=188.75ms min=168.8ms  med=188.75ms max=208.7ms  p(90)=204.71ms p(95)=206.7ms 
     browser_web_vital_ttfb......: avg=55.1ms   min=26.29ms  med=55.1ms   max=83.9ms   p(90)=78.14ms  p(95)=81.02ms 
     checks......................: 100.00% ✓ 2        ✗ 0  
     data_received...............: 0 B     0 B/s
     data_sent...................: 0 B     0 B/s
     iteration_duration..........: avg=1.53s    min=1.53s    med=1.53s    max=1.53s    p(90)=1.53s    p(95)=1.53s   
     iterations..................: 1       0.394187/s
     vus.........................: 1       min=1      max=1
     vus_max.....................: 1       min=1      max=1

- browser_data_received: The amount of data received by the browser.

- browser_data_sent: The amount of data sent by the browser.

- browser_http_req_duration: Average duration of HTTP requests 

- browser_http_req_failed: The percentage of failed HTTP requests.

- browser_web_vital_cls: Cumulative Layout Shift, a measure of visual stability.

- browser_web_vital_fcp: First Contentful Paint, the time it takes for the first piece of content to be rendered.

- browser_web_vital_fid: First Input Delay, the time it takes for the page to respond to the first user interaction.

-  browser_web_vital_lcp: Largest Contentful Paint, the time it takes for the largest content element to be rendered.

- browser_web_vital_ttfb: Time to First Byte, the time it takes for the browser to receive the first byte of the response from the server.

Conclusion

k6 represents a new stage in the field of load testing with the browser module. By bridging the gap between protocol-level and browser-level testing, k6 enables organisations to gain insight into the end-user experience. You can collect browser-specific metrics and ensure that everything is performed properly on the frontend, even under load. 

While the k6 browser module is still in its early stages, it is possible to perform comprehensive load testing while also combining protocol-level and browser-level tests into a single script.

As the demand for high-performance, user-friendly web applications continues to grow, the k6 Browser module positions itself as a toolkit that equips developers, performance engineers and QA professionals with the tools necessary to deliver digital ecosystems.

Elif Ozcan

Software Test Consultant @kloia