In this tutorial, we explain why and how to use Playwright for API testing, as well as provide an in-depth tutorial that will walk you through a live example of API testing.
Playwright is a great tool, build by Microsoft, primarily for end-to-end UI testing. But, Playwright also has excellent API capabilities you can leverage to test server functionality, setup and teardown context for your tests, or verify your backend state.
When to Use Playwright for API testing?
There are a few use-cases for Playwright for API testing:
1. Assert Correct Backend State
As part of your end to end test, you might want to query the backend directly, to ensure changes are reflected in your DB. It’s usually best, to verify changes through the UI. That way you make sure that the entire experience works. But, when the data is not presented, for example when a certain action should trigger an email sent, you should use Playwright API tests
2. Prepare the app state for testing (e.g. login)
Before every test case, you’ll need to prepare the app state. The more obvious part is logging in, but for example, if you have a test that edits an entity in your app, you might want to create that entity before the test. That’ll help you keep the data consistent.
Either way, preparing the state through the API rather than the UI is preferable for two reasons:
- Speed: calling to the API directly is faster
- Isolated: When testing a functionality, you want to isolate the test as much as possible. Doing the setup through an API, reduces the chances that bugs in other part of your app that are unrelated to the current test case, will affect it.
3. Backend API Testing
Even outside the scope of UI testing, Playwright can be a great tool to simply test your API gateway. Using Playwight allows you to consolidate setup, config and pipelines between your UI and Backend tests.
Tutorial Overview
This tutorial will demonstrate how to use Playwright for API testing on Conduit -an open-source Medium clone.
We will create a simple test that edits an article title. For that we will do a few things:
- Setup global authentication using Conduit API, store the auth token in
.auth
file to be used by all tests - Create a spec file for all tests related to “Articles Edit”.
- In the beforeEach hook, make an API request to create an article. Remember, we don’t want to test the “Create article” feature, only create one for the other tests.
- In the test itself, edit the article using the UI. Then validate that the change happened by querying the backend
- In the afterEach hook, delete the article.
Authenticate using Playwright APIRequest class.
- Go to your project directory and run the following command. Answer the questions using the default values to get your project set up.
npm init playwright@latest
- Delete all files inside the
tests
directory and create a file namedauth.setup.ts
with the following code:
import { expect, test as setup } from "@playwright/test";import { writeFileSync } from "fs";
// Setting the project parameters. This will usually be set in an environment variable or Playwright configconst username = "pw-api-tutorial@example.com";const password = "1234";const baseAPIURL = "https://api.realworld.io/api/";const authFile = ".auth";
setup("authenticate", async ({ request }) => { const authenticationReq = await request.post(baseAPIURL + "/users/login", { data: { user: { email: username, password: password } }, }); // verify API response status code is 200 expect(authenticationReq.ok()).toBeTruthy(); const response = await authenticationReq.json(); const jwtToken = response.user.token; const storageState = { cookies: [], origins: [ { origin: "https://demo.realworld.io", localStorage: [ { name: "jwtToken", value: jwtToken, }, ], }, ], }; writeFileSync(authFile, JSON.stringify(storageState));});
Let’s go over what the code above does:
- Makes a POST with the user name and password in the request body to authenticate through the API.
- Validates that we received a 200 status code in the api response.
- Saves the token from the response body to
.auth
so we can read it later in the spec files. Conduit authentication works by saving the token to the browser local storage, under the value “jwtToken”, so that’s why we are not just saving the token as is, but constructing it into a storageState object.
Next open playwright.config.ts
and replace the projects key with the following configuration options:
projects: [ { name: "setup", testMatch: /.\*\.setup\.ts/ }, { name: "chromium", use: { ...devices["Desktop Chrome"], storageState: ".auth" }, dependencies: ["setup"], },],
You can read more about this setup here, but in short we define a setup project that runs auth.setup.ts
file. Then, we config our test project have a dependency on “setup” and load .auth
to the browser context so we are already authenticated witnin our test file.
Finally run:
npx playwright test
And make sure our test passes
Use Playwright to setup test context
Under /tests
folder create a file named ArticleEdit.spec.ts
. Add the following code
import { test, expect, request } from "@playwright/test";
// Setting the test parameters. This will usually be set in an environment variable or Playwright configconst baseAPIURL = "https://api.realworld.io/api/";const baseFrontendURL = "https://demo.realworld.io/";
// Creating global objects to be set in beforeAll. This can also be defined in Playwright config for the entire project.
function generateRandomString() { return (Math.random() + 1).toString(36).substring(2);}
const articleData = { article: { title: "New Test Article " + generateRandomString(), // we add a random string since title must be unique description: "This is a test", body: "# Test markdown", tagList: [], },};
let articleObject;let jwtToken;let apiContext;
console.log(articleData);
test.beforeEach(async ({ page }) => { await page.goto(baseFrontendURL); jwtToken = await page.evaluate(() => localStorage.getItem("jwtToken")); apiContext = await request.newContext({ baseURL: baseAPIURL, extraHTTPHeaders: { Authorization: `Token ${jwtToken}` }, }); const createArticleReq = await apiContext.post("articles", { data: articleData, }); expect(createArticleReq.ok()).toBeTruthy(); articleObject = (await createArticleReq.json()).article; await page.goto(baseFrontendURL + "#/editor/" + articleObject.slug);});
test("Add a tag to an article post publish", async ({ page }) => { // TBD});
A few things are going on in this code snippet:
- Get the JWT from local storage, so we can use it when making API requests .
- Create a new APIRequestContext with a base URL and an Authorization header. Going forward, every time we make a request, the auth token will already be included.
- Make a POST request to create an article and save the response into
articleObject
- Go to the editor URL so we are ready to begin testing.
All of this is happening in the beforeEach hook, meaning that this setup code will run before every test.
Go head and run the test. After you run the test open your browser and go to https://demo.realworld.io/#/@pw-api-tutorial login with username pw-api-tutorial@example.com
and password 1234
and verify that the article was created.
Use Playwright to teardown a test
Before we write the test itself, let’s just delete the article in the afterEach hook. Add the code snippet below to your test:
test.afterEach(async ({ page }) => { const deleteArticleReq = await apiContext.delete( "articles/" + articleObject.slug ); expect(deleteArticleReq.ok()).toBeTruthy();});
This code add an afterEach hook that will run after every test in the ArticleEdit.spec.ts
file and will delete the article that was created in the beforeEach
hook.
Use Playwright to verify DB state after an action
Now that we have hooks that create and delete an article, we are ready to write the test itself. Add the following code to ArticleEdit.spec.ts
test("Add a tag to an article post publish", async ({ page }) => { // Add tag await page.getByPlaceholder("Enter tags").click(); await page.getByPlaceholder("Enter tags").fill("first-tag"); await page.getByPlaceholder("Enter tags").press("Enter"); await page.getByPlaceholder("Enter tags").fill("second-tag"); await page.getByPlaceholder("Enter tags").press("Enter"); await page.getByRole("button", { name: "Publish Article" }).click();
// Assert the tag elements are visible await expect(page.getByText("first-tag")).toBeVisible(); await expect(page.getByText("second-tag")).toBeVisible(); // verify correct backend state const getArticleReq = await apiContext.get("articles/" + articleObject.slug); expect(getArticleReq.ok()).toBeTruthy(); const articleWithTags = (await getArticleReq.json()).article; expect(articleWithTags.tagList).toHaveLength(2); expect(articleWithTags.tagList).toContain("first-tag"); expect(articleWithTags.tagList).toContain("second-tag");});
The code above is pretty straight forward. At the first section, we add a tag, then, assert that the tag elements are visible, and finally we use the API to get the article object and assert the tags are there.
One last time, run npx playwright test
. Let’s review the html report
Final thoughts
In this tutorial, we’ve demonstrated how you can use Playwright API testing feature to setup seed data for your tests, keep your data consistent, verify the backend state and finally clean up the data. Using the API results in a more isolated, faster and overall robust tests.
If you’d like to take this tutorial even further, there are a few paths to improve our setup:
Use fixtures to create a global APIRequestContext
In the test we wrote, we create an APIRequestContext that automatically has a baseURL and adds the JWT to the headers. But want if we want to create a global APIRequestContext that will be used across test files?
One way is to expend the request fixture. We will not go into that in details in this article, but you should go over the official Playwright docs.
Develop a data factory to create articles and seed data more consistently
There might be many other tests that need to create and delete an article via the API as part of the test. A more robust way to approach these kind of tasks, is to create a “Data Factory”. Essentially a set of functions that create fake data objects and use the API to generate them. So in a test, when you need to create an article, all you need to do is write something like DataFactor.Articles.createArticle()
. You can use the Faker library to easily create randomized fake data.
![Avatar of Gal Vered](/_astro/gal_vered_avatar.C_cm3hS__Z1AG4S9.webp)
Gal Vered
Gal Vered is a Co-Founder at Checksum where they use AI to generate end-to-end Cypress and Playwright tests, so that dev teams know that their product is thoroughly tested and shipped bug free, without the need to manually write or maintain tests.
In his role, Gal helped many teams build their testing infrastructure, solve typical (and not so typical) testing challenges and deploy AI to move fast and ship high quality software.