This content originally appeared on DEV Community and was authored by Alicia Marianne
Is pretty common link perfomance testing with backend applications, but what if I say to you that you can automate and get key metrics that will help you improve the performance of your frontend applications? That is what we are going to learn in this tutorial.
Key concepts for Browser performance
Before we go to the hands on, we need to understand what is the browser performance testing.
Browser-based load testing verifies the frontend performance of an application by simulating real users using a browser to access your website.
This type o testing provides a way to mesure user experience and find issues that are difficult to catch on the protocol level. When running the tests, we will be able to answers questions like:
- When my application is receiving thousands of simultaneous requests from the protocol-level, what happens to the frontend?
- How can I get metrics specific to browsers, like total page load time?
- Are all my elements interactive on the frontend?
- Are there any loading spinners that take a long time to disappear? How we will use for this tutorial k6, we need to understand some concepts about the framework itself:
- Browser type: A browser script must have the
options.browser.type
field set in theoptions
object. - Asynchronous operations: Browser interactions (like navigation and clicks) are asynchronous, so your test must use
async
/await
. Refer toAsynchronous operations for more details. - Locators: The Locator API can be used to find and interact with elements on a page, such as buttons or headers. There are other ways to find elements, such as using the
Page.$()
method, but the Locator API provides several benefits, including finding an element even if the underlying frame navigates, and working with dynamic web pages and SPAs such as React. ## Setup of the project Now that we know the key concepts, we can start to create our project. For this tutorial, we will use the e-commerce application [Swag Labs]( https://www.saucedemo.com ) and get the metrics for the main workflow: Login → Add items to the cart → CheckOut For it you need to: - Have a latest version of node and npm installed
- [Install K6]( https://grafana.com/docs/k6/latest/set-up/install-k6/ )
Project structure
k6-tests/
├── libs/
│ └── addToCart.js
│ └── auth.js
│ └── checkOut.js
├── pages/
│ └── CheckOutOverview.js
│ └── CheckoutPage.js
│ └── DashboardPage.js
│ └── LoginPage.js
│ └── SuccessPage.js
├── tests/
│ └── buyItemsPeformanceGlith.js
│ └── buyItemsUserA.js
├── utils/
│ └── k6-options.js
Creating the tests
Is important to mention that k6 runs only one test per time
The basic structure of a K6 project is:
- Imports
- Define options
- Setup
- Default function: The default exported function is the entry point for the test script. It will be executed repeatedly the number of times you define with the
iterations
option. In this function, usingasync
/await
, add the functions that open a browser page, interacts with the elements on the page, takes a screenshot, and performs acheck.
Knowing it, we can write our tests in the following model:
buyItemsUserA.js
import {performLogin} from '../libs/auth.js';
import {addItemsToCart} from '../libs/addToCart.js';
import {browserOptions} from '../utils/k6-options.js';
import {checkOut} from '../libs/checkOut.js';
export const options = browserOptions;
export default async function () {
const page = await performLogin('standard_user', 'secret_sauce');
await addItemsToCart(page);
await checkOut(page);
await page.close();
}
Now, we need to understand what which function does.
browserOptions
To avoid write for all tests the same configuration, we will create on the utils
folder the file k6-options.js
where we will add the basic configuration for our test:
export const browserOptions = {
scenarios: {
ui: {
executor: 'shared-iterations',
vus: 1,
iterations: 15,
options: {
browser: {
type: 'chromium',
},
},
},
},
};
Here we define the number of interactions we want, the number of instances, and the type of browser that will be used.
performLogin
On the lib
folder we will create a function that will be responsible to perform the login and return the page context to our tests:
import { browser } from 'k6/browser';
import { check } from 'k6';
import { LoginPage } from '../pages/LoginPage.js';
/**
* Do the login and return the page authenticated.
* @param {string} username
* @param {string} password
* @param {string} url
* @returns {Promise<Page>}
*/
export async function performLogin(username, password, url = 'https://www.saucedemo.com/') {
const page = await browser.newPage();
const loginPage = new LoginPage(page);
await loginPage.goto(url);
await loginPage.login(username, password);
const loginMessage = await loginPage.getHomePageMessage();
check(page, {
'Login done with success': () => loginMessage === 'Products',
});
return page;
}
The performLogin will call the LoginPage.js
class that contain the locators, and methods for the login:
export class LoginPage {
constructor(page) {
this.page = page;
this.usernameField = page.locator('#user-name');
this.passwordFiled = page.locator('#password');
this.loginButton = page.locator('#login-button');
this.homePageHeader = page.locator('.title');
}
async goto(){
await this.page.goto('https://www.saucedemo.com/');
}
async login(username, password) {
await this.usernameField.type(username);
await this.passwordFiled.type(password);
await this.loginButton.click();
}
async getHomePageMessage(){
return this.homePageHeader.textContent();
}
}
addItemsToCart
The method addItemsToCart will be responsible to add items to the cart and make sure that they were added with success using the DashboardPage.js
methods
import { check } from 'k6';
import {DashboardPage} from '../pages/DashboardPage.js';
/**
* Receives the page context and add items to the cart
* @param {Page} page
*/
export async function addItemsToCart(page) {
const dashboardPage = new DashboardPage(page);
await dashboardPage.addCart();
let count = await dashboardPage.getCartCount();
check(page, {
'Items': () => count === '3',
});
}
DashboardPage.js
export class DashboardPage {
constructor(page) {
this.page = page;
this.addCartBackPack = page.locator('[data-test="add-to-cart-sauce-labs-backpack"]');
this.addCartboltTShirt = page.locator('#add-to-cart-sauce-labs-bolt-t-shirt');
this.addCartonesieRip = page.locator('#add-to-cart-sauce-labs-onesie');
this.cartIcon = page.locator('[data-test="shopping-cart-badge"]');
}
async addCart(){
await this.addCartBackPack.click();
await this.addCartboltTShirt.click();
await this.addCartonesieRip.click();
}
async getCartCount(){
await this.cartIcon.isVisible();
return await this.cartIcon.textContent();
}
}
checkOut
The checkOut method make all the process of checkout:
import {CheckoutPage} from "../pages/CheckoutPage.js";
import {SuccessPage} from "../pages/SuccessPage.js";
import {CheckOutOverview} from "../pages/CheckOutOverview.js";
/**
* Receives the page context and add items to the cart
* @param {Page} page
*/
export async function checkOut(page) {
const checkoutPage = new CheckoutPage(page);
const checkoutOverview = new CheckOutOverview(page);
const successPage = new SuccessPage(page);
await checkoutPage.checkOut('John', 'Doe', '12345');
await checkoutOverview.confirmCheckOut();
await successPage.getSuccessMessage();
}
For checkout, different from te other pages, we have three different pages when doing it:
- First one to access the cart and confirm the items
- Second to add the information
- Third page of Success
So for it, we will create three pages as well:
CheckOutPage.js
– Click on the cart and redirect to the CheckOut Overview page
export class CheckoutPage {
constructor(page) {
this.page = page;
this.cartIcon = page.locator('[data-test="shopping-cart-link"]');
this.checkoutButton = page.locator('[data-test="checkout"]');
this.checkInformation = page.locator('[data-test="title"]');
this.firstName = page.locator('#first-name');
this.lastName = page.locator('#last-name');
this.zipCode = page.locator('#postal-code');
this.continueButton = page.locator('[data-test="continue"]');
}
async goCheckoutPage(){
await this.cartIcon.click();
await this.checkoutButton.click();
return await this.checkInformation.textContent();
}
async addInformation(firstName, lastName, zipCode){
await this.firstName.type(firstName);
await this.lastName.type(lastName);
await this.zipCode.type(zipCode);
await this.continueButton.click();
}
async checkOut(firstName, lastName, zipCode){
await this.goCheckoutPage()
await this.addInformation(firstName, lastName, zipCode);
}
}
CheckOutOverview.js
– Confirm the informations and what is going to be the payment method
import { check } from 'k6';
export class CheckOutOverview{
constructor(page){
this.page = page;
this.chekoutOverview = page.locator('[data-test="title"]');
this.finishButton = page.locator('[data-test="finish"]');
}
async confirmCheckOut(){
const checkOutOverviewTitle = await this.chekoutOverview.textContent();
check(this.page, {
'CheckoutOverview': () => checkOutOverviewTitle === "Checkout: Overview",
})
await this.finishButton.click();
}
}
SuccessPage.js
– Confirmation of the payment
import { check } from 'k6';
export class SuccessPage {
constructor(page){
this.page = page;
this.successMessage = page.locator('[data-test="complete-header"]');
this.fullMessage = page.locator('[data-test="complete-text"]');
this.backHomeButton = page.locator('[data-test="back-to-products"]');
}
async getSuccessMessage(){
const successMessageText = await this.successMessage.textContent();
const fullMessageText = await this.fullMessage.textContent();
check(this.page, {
'Success short message': () => successMessageText === "Thank you for your order!",
'Success full message': () => fullMessageText === "Your order has been dispatched, and will arrive just as fast as the pony can get there!",
})
await this.backHomeButton.click();
}
}
Running the tests and generating reports
To run the tests with K6 you can just open the terminal and run the following command:
k6 run ./tests/buyItemsUserA.js
We will see a lot of information and different metrics, to help us to understand the results, we can use the dashboard provided by k6. For it, you just need to add on the command that K6 will open a local server with the results of your execution in real time.
K6_WEB_DASHBOARD=true K6_WEB_DASHBOARD_OPEN=true k6 run ./tests/buyItemsUserA.js
If you want only the summary of the execution, you can ask k6 to generate the html report:
k6 run ./tests/buyItemsUserA.js K6_WEB_DASHBOARD=true K6_WEB_DASHBOARD_EXPORT=results/report.html
The metrics
Now that we have the tests, the results, we need to understand the results provided by k6. The metrics provided by K6 are based on Core Web Vitals
The vitals are composed of three important metrics to help a user experience when using your web application:
– Longest content display (LCP): measures loading performance. To offer a good user experience, LCP needs to occur within 2.5 seconds of the page starting to load.
– Interaction to Next Paint (INP): measures interactivity. To offer a good user experience, pages need to have an INP of 200 milliseconds or fewer.
– Cumulative Layout Shift (CLS):measures visual stability. To offer a good user experience, pages need to maintain a CLS of 0.1 or less
These are the metrics that are stable now on the Core Web Vitals page; They help us understand which areas of our frontend application need optimization.
Existing browser measures, such as Load and DOMContentLoaded times, no longer accurately reflect user experience very well. Relying on these load events does not give the correct metric to analyze critical performance bottlenecks that your page might have. Google’s Web Vitals is a better measure of your page performance and its user experience.
For this test, if we only run once, we had the following results:
A quick analyze from the results, is that when we add more interactions, our browser_web_vital_lcp
, responsible to measures the time it takes for the largest content element on a page to become visible is less than 1 second, what is acceptable, but how it incresed from 362ms to 799ms is important to investigate the number of users that will pass more than 2 seconds to load the content and from there, improve the software.,
The more important of this reports, is to have them running with a number of interations and virtual users expected for the product that will be tested and from there, plan improvements.
Conclusion
This tutorial guided us through creating our first browser-based performance test using k6 browser. We also explored how to structure the project using the Page Object Model for better maintainability and scalability. Finally, we learned how to generate dashboards to help analyze the performance test results effectively.
You can check this project here.
This content originally appeared on DEV Community and was authored by Alicia Marianne