API Contract Testing on Frontend with Playwright

Sometimes, as a test engineer, business requirements for testing may be quite weird, and you have to adopt different types of testing in one suite.

Andrey Enin
6 min readDec 25, 2023
Midjourney prompt
Midjourney prompt: http api contact test frontend application, 2d abstract scheme, engineering drawing, kandinsky style, white background

Contract testing is a type of software testing that focuses on verifying the interaction between separate components/services (more often, it is two microservices). When two microservices interact via the API, one service sends requests in a predefined format, and another responds in a predefined format. This format is called a «contract» — an agreement between services (and even dev teams) on how they commit to communicating with each other.

A «contract» can be an API specification, but more often, it is just a request and response bodies’ schemas as JSON files, which are shared between two services, and both of them test their APIs against these schemas — this approach is even distinguished into a separate testing method «schema-based contract testing».

In the case of client-server architecture, the frontend can act as a consumer or provider for various APIs and vice versa:

Frontend application as a consumer or provider for contract testing
Fig. 1. Frontend application as a consumer or provider for contract testing

In many articles (see links at the end of the article), contract testing is opposed to integration or end-to-end testing. But in this article, I want to show that contract testing can be a part of end-to-end testing — it can be just a tool for specific checks.

This occasion can happen in the case of specific business requirements for frontend autotests, for example, to check that your frontend makes specific requests to third-party APIs in a particular format. In other words, to ensure that UI sends correct data.

Moreover, these third-party APIs may not allowed to be requested during the tests. That looks like an obvious necessity to «close» these third-party APIs by mocks, but what if you don’t have any complex mocking infrastructure on your test project (and/or don’t want to have)? It can be so if your tests run against a live staging environment. In such cases, you can «close» requests to third-party APIs on a network level by Playwright.

It was not a problem to find a similar project on the Internet. There are a lot of DeFi startups who use open APIs for their infrastructure, but these APIs are mostly GraphQL and JSON-RPC — which adds a little bit of complexity to the example. The description of their differences from REST API is not the topic of this article.

At least I found Sushi cryptocurrency swap page, whose frontend does just a few desired POST requests to third-party API (API’s URL is different from the current website):

Website’s frontend does POST requests to third-party APIs
Fig. 2. Website’s frontend does POST requests to third-party APIs

The same case in a scheme representation looks like this:

Website’s frontend does POST request to third-party API
Fig. 3. Website’s frontend does POST request to third-party API

Let me remind you that I focus on third-party APIs because checking an internal API is not the case for this article — you can check your internal APIs by your internal API tests and/or integration ones.

In contract testing, it is implied that each component/service is isolated from each other. Here, you can easily isolate frontend from third-party API with Playwright’s network capabilities:

  1. Roughly abort request — the request will not be sent to an external API;
  2. Or mock it.

For the second case, you can modify the response with the abortion of the request if you use only fulfill() class. But if you use fulfill() with fetch(), the request will be sent to the external API. In both ways, when you fulfill the response body with JSON — you make contract testing (checking that the client is processing the fulfilled response) if this JSON scheme is the same as the one used for testing on the side of the external API.

For both cases, you intercept the request by waitForRequest() class for testing POST’s request body against your contract (and, of course, for PUT or PATCH methods):

HTTP request interception through Playwright
Fig. 4. HTTP request interception through Playwright

If your request body is in JSON format (I think this will happen 90 percent of the time), you can instantly use postDataJSON() class for comparison JSON schemes by your favorite tool: Ajv, Zod, or use toEqual() assertion, if for some reason you decide to compare the two JSON objects head-on.

While you are checking only the request’s contract, you may not need the response and may simply abort it (attention, the right behavior depends on your application, and maybe you have to mock the response to prevent the application’s crash):

How route.abort() works in Playwright Inspector
Fig. 5. How route.abort() works in Playwright Inspector

Here is the code example of such a test:

import { expect, type Page, test } from '@playwright/test';
import { z } from 'zod';

// Contract
const schema = z.object({
jsonrpc: z.string(),
id: z.number(),
method: z.string(),
params: z.array(z.union([z.string(), z.boolean()])),
});

let page: Page;

test.beforeAll(async ({ browser }) => {
const context = await browser.newContext();
page = await context.newPage();

await page.route(/.+lb\.drpc\.org\/ogrpc\?network=ethereum.+/, async (route) => {
if (route.request().method() === 'POST') {
await route.abort();

return;
}
},
);
});

test('Open Sushi Swap', async () => {
// Waiting for a request should be before .goto() method,
// because desired request can be done before the page is fully loaded.
const requestPromise = page.waitForRequest(
(request) =>
request.url().includes('lb.drpc.org/ogrpc?network=ethereum') &&
request.method() === 'POST',
);

await page.goto('/swap');

const request = await requestPromise;
await expect(
() => schema.parse(request.postDataJSON()),
'Should have a request by the contract',
).not.toThrowError();
});

Where,

  • const schema is a scheme declaration in Zod’s format;
  • In beforeAll hook, all POST requests to URLs match https://lb.drpc.org/ogrpc?network=ethereum&dkey=Ak765fp4zUm6uVwKu4annC8M80dnCZkR7pAEsm6XXi_w are blocking;
  • const requestPromise receives data from the first request matches https://lb.drpc.org/ogrpc?network=ethereum&dkey=Ak765fp4zUm6uVwKu4annC8M80dnCZkR7pAEsm6XXi_w;
  • In expect() assertion, the reference scheme is parsed against the request’s data. The test passes if the parsing/validating process does not fail — toThrowError().

The test presented above may contain more steps and checks because the contract’s check may be just a part of the end-to-end suite.

Read more about contract testing:

Furthermore, in theory, the same approach to mocking can be applied to all HTTP API requests on frontend:

Mock APIs
Fig. 6. Mock APIs

Read more about how Playwright mocks API.

This article was featured in:

--

--

Andrey Enin

Quality assurance engineer: I’m testing web applications, APIs and do automation testing.