Now most of websites and apps use API to get and process the data of the website, you can use the same API for the UI to improve your tests.
I will share some tips where I am using API for improve my playwright test. You can use the same approach for the other frameworks.
Login with API instead of UI
For most pages, you can log in with a login API request and store the JWT returned in a cookie or local storage. To improve the speed of your test, you can log in with the API and set the local storage or cookie by code.
I created an ApiHelper class containing all the API common methods. If the API functions in Playwright change in the future, you only need to change this file.
import { APIRequestContext, APIResponse, Page } from '@playwright/test';
import { request } from '@playwright/test';
export class ApiHelper {
constructor(private page: Page, private baseUrl: string) {
}
/**
* Create a request with token from localStorage
*/
async createRequest(baseURL: string) {
const token = await this.page.evaluate('localStorage["token"]');
const apiRequest: APIRequestContext = await request.newContext({
baseURL: baseURL,
extraHTTPHeaders: {
'Authorization': `Bearer ${token}`,
}
});
return apiRequest;
}
/**
* Wait for response from url contains the api url
* @param apiUrl api url to wait until get the response
* @param statusCode Status code returned by the api
* @returns responsePromise
*/
waitForResponse(apiUrl: string, statusCode = 200, method: 'POST' | 'GET' | 'PUT' | 'DELETE' = 'GET') {
const responsePromise = this.page.waitForResponse(response => response.url().includes(apiUrl) && response.request().method() == method
&& response.status() == statusCode);
return responsePromise;
}
/**
* Call to api post
* @param url post url (not base url is needed)
* @param data data to post
* @returns
*/
async get(url: string): Promise<APIResponse> {
const apiRequest = await this.createRequest(this.baseUrl);
return await apiRequest.get(url);
}
/**
* Call to api post
* @param url post url (not base url is needed)
* @param data data to post
* @returns
*/
async post(url: string, data: any): Promise<APIResponse> {
const apiRequest = await this.createRequest(this.baseUrl);
return await apiRequest.post(url, { data: data });
}
/**
* Call to api post
* @param url post url (not base url is needed)
* @param data data to post
* @returns
*/
async put(url: string, data: any): Promise<APIResponse> {
const apiRequest = await this.createRequest(this.baseUrl);
return await apiRequest.put(url, { data: data });
}
/**
* Call to api delete
* @param url delete url (not base url is needed)
* @returns
*/
async delete(url: string): Promise<APIResponse> {
const apiRequest = await this.createRequest(this.baseUrl);
return await apiRequest.delete(url);
}
async mockApi(url: string, jsonData: any) {
await this.page.route(url, async route => {
await route.fulfill({ body: JSON.stringify(jsonData) });
});
}
}
With Playwright, you can save the current state of your page (local storage and cookies) in a JSON file and reuse the state for your test. You can check my previous article:
Mock an API to get the same values
With a mock function you can replace the real result of an API with a fixed data, with this you can always have the same result. Too you can change the status code to simulate some error in the server and check if the UI could manage this scenario.
You can mock the API for A/B testing to get both options to get always one of both options.
You can prevent connecting to some analytics for example google analytics and stats for your users and discards your E2E regression tests. You can prevent that calls to the analytics API.
Finally, you can simulate some errors like 401, 403, or 500 to check how the error that the UI will return in all of those scenarios.
Get the same results for an API useful in dashboards. You can check my previous article about how to mock the API for a Accounts Receivable dashboard.
Generate preconditions or postconditions
You can generate some preconditions with API to isolate your tests, run in parallel in any order.
One example is the test case for edit a customer:
You can create the customer by API, you don’t need to create by UI because it will be slower and you are only interested in edit not in create.
Test the edit of the customer with the UI.
Delete the customer created by API to prevent add a customer in each test execution.
It’s better if you use random data to generate the values instead of use the same hard code values. You can use fakerJS to generate random data.
In this sample, I will create a server with API for the edit test case.
First, I will add an interface with the JSON needed to create a server.
export interface Server {
Key: number
Name: string
Url: string
}
Now, I will create a ServerApi with the functions to:
Create a server
Delete the server
import test, { Page } from '@playwright/test';
import { ApiHelper } from '../../utils/ApiHelper';
import { Server } from '../models/Effiziente/Server';
export class ServerApi {
apiHelper: ApiHelper;
api = 'api/Server';
constructor(private page: Page) {
const baseURL = process.env.EFFIZIENTE_API_URL ? process.env.EFFIZIENTE_API_URL : 'https://effizienteauthdemo.azurewebsites.net';
this.apiHelper = new ApiHelper(this.page, baseURL);
}
/**
* Delete a server by api
* @param id Server id
*/
async deleteServer(id: number) {
await test.step('Delete server with id: "' + id + '"', async () => {
const apiResponse = await this.apiHelper.delete(this.api + '/' + id.toString());
return apiResponse;
});
}
/**
* Create a server with api
* @param server Data for the server
* @returns api response
*/
async createServer(server: Server) {
const apiResponse = await this.apiHelper.post(this.api, server);
return apiResponse;
}
}
You can delete the server added with API in the before each hook in Playwright that is executed after each test is finished.
To generate the data for edit and create a server you can use fakerJS to generate random data to create and edit the server, with random data you can execute your test in parallel and prevent duplicates or always test with the same data.
In this sample I added the tests to edit and delete a server
import { expect, test } from '@playwright/test';
import { faker } from '@faker-js/faker';
import { Server } from '../../../api/models/Effiziente/Server';
import { AddServerPage } from '../../../pages/Effiziente/addServerPage';
import { ServersPage } from '../../../pages/Effiziente/serversPage';
import { AnnotationType } from '../../../utils/annotations/AnnotationType';
test.describe('Servers', () => {
let id = 0;
test.use({ storageState: 'auth/admin.json' });
test('Should edit a server', async ({ page }) => {
const serversPage = new ServersPage(page);
const key = faker.number.int({ min: 2, max: 999_998 });
const newKey = key + 1;
await serversPage.goTo();
//Check if exists a server with key if not exists create one with API
const response = await serversPage.serverApi.getServerByKey(key.toString());
if (response.status() == 204) {
//Create and server by api to test edit to remove dependenciees for the create with UI
const server: Server = {
Key: key,
Name: faker.company.name(),
Url: faker.internet.url()
};
id = await serversPage.createServer(server);
}
else {
//Get the id for the server key with id 3 to delete and isolate the test
const responseText = JSON.parse(await response.text());
id = +responseText.Id;
}
const newName = faker.company.name();
const newUrl = faker.internet.url();
//Go to page again to get the server creted by api
await serversPage.goTo();
await serversPage.table.clickInEditByKey(key);
await serversPage.key.fill(newKey.toString());
await serversPage.name.fill(newName);
await serversPage.url.fill(newUrl);
await serversPage.save.click();
await serversPage.checkSuccessMessage();
await serversPage.checkRow(newKey, newName, newUrl);
});
test.afterEach(async ({ page }) => {
//Delete the server created after each test
const addServerPage = new AddServerPage(page);
if (id > 0)
await addServerPage.serverApi.deleteServer(id);
});
});
To select a random item in an e-commerce site
One of the main user journeys in an e-commerce site is the flow to add an item to the cart. Instead of selecting the same item, you can get the list of items with the API endpoint that shows the items in your website and select a random article. You have better test coverage because test with different items. One time I found on the production e-commerce site, a Lorem ipsum item that was not available to buy.
For this test, I am using the Contoso Traders an e-commerce site to test.
First, I created the ProductApi file with the function to wait and get the product from the products API
import { Page } from '@playwright/test';
import { ApiHelper } from '../../utils/ApiHelper';
export class ProductsApi {
apiHelper: ApiHelper;
api = '/v1/products';
constructor(private page: Page) {
const baseURL = process.env.CONTOSO_API_URL ? process.env.CONTOSO_API_URL : 'https://contoso-traders-productsctprd.eastus.cloudapp.azure.com';
this.apiHelper = new ApiHelper(this.page, baseURL);
}
/**
* Wait for the server is created
*/
waitForGetAllProducts() {
return this.apiHelper.waitForResponse(this.api + '/?type=all-products');
}
}
After I created a function to click on the All Products link and store the products in a property
async allProductsClick() {
const responsePromise = this.productsApi.waitForGetAllProducts();
await this.allProducts.click();
const responseCreate = await responsePromise;
this.products = JSON.parse(await responseCreate.text()).products;
}
I created a selectRandomProduct that gets a random item and selects it with the image because you can have the same item name with a different image name.
async selectRandomProduct() {
const productIndex = Math.floor(Math.random() * this.products.length);
this.product = this.products[productIndex];
await this.page.locator('div[style*="'+ this.product.imageUrl+'"]').click();
}
And finally, you can check the item added to the cart.
import { expect, test } from '@playwright/test';
import { HomePage } from '../../../pages/ContosoTraders/homePage';
import { ProductDetailPage } from '../../../pages/ContosoTraders/productDetailPage';
import { CartPage } from '../../../pages/ContosoTraders/cartPage';
import { AnnotationType } from '../../../utils/annotations/AnnotationType';
test.describe('Contoso Traders', () => {
// eslint-disable-next-line playwright/expect-expect
test('Should add an item to the cart', async ({ page }) => {
const homePage = new HomePage(page);
await homePage.goTo();
await homePage.allProductsClick();
//Select a random product from api results by image
//because the name could be the same
await homePage.selectRandomProduct();
const productDetailPage = new ProductDetailPage(page);
await productDetailPage.addToBag.click();
let assertDescription = 'There is one item in the bag';
await productDetailPage.addAnnotation(AnnotationType.Assert, assertDescription);
await expect(productDetailPage.shoppingCartItems.locator, assertDescription).toContainText('1');
await productDetailPage.bag.click();
const cartPage = new CartPage(page);
//Check the item in the shopping cart should be the same that was selected
assertDescription = `The name of the item in the cart is: ${homePage.product.name}`;
await cartPage.addAnnotation(AnnotationType.Assert, assertDescription);
await expect(cartPage.cartItem.name, assertDescription).toHaveText(homePage.product.name);
//Format price to currency format
const price = homePage.product.price.toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
});
assertDescription = `The price of the item in the cart is: ${price}`;
await cartPage.addAnnotation(AnnotationType.Assert, assertDescription);
const unitPrice = await cartPage.cartItem.unitPrice.first().innerText();
await expect(unitPrice, assertDescription).toEqual(price);
});
});
You can see the full repository with my playwright framework template.
Thank you for reading. If you like this article, feel free to share or add a comment with your suggestions for the following topics.