Sponsored Link
Looking for a unified way to  viewmonitor, and  debug Playwright Test Automation runs?
Try the Playwright Dashboard by today. Use the coupon code PWSOL10 for a 12 month 10% discount.

How do you define an apiURL along with the baseURL in a Playwright Test?

I was recently asked how I handle an apiURL along with the baseURL in my Playwright Tests. One of the big benefits you get out of the box with Playwright is the ability to make API calls to create data or intercept network requests in order to have data to assert against. This isn't a problem everyone will face, but if you're API has a different URL than your UI I hope this article will be helpful.

There are really 4 ways to solve this problem that I've found.

  1. Hardcode your API URL in your tests
  2. Use Environment variables with process.env
  3. Use a Class to create public values
  4. Use Fixtures I'll walk through 2 different ways to do this

For the examples I'll be using Practice Software Testing, a modern demo site for checkout experience, Created by Roy De Kleijn. A few resources to learn more about this site

Hardcode your API URL in your tests

The first example, just yeeting out the API URL throughout your code, is not a practice I would recommend, as if you ever want to test against a local environment or a staging or sandbox environment, you will have a lot of places in code that need to be updated, which isn't ideal.

Use Environment variables with process.env

The second example is utilizing environment variables throughout your code. This isn't the best approach either but it is a good way to get your project off the ground. Once you have a fair amount of tests you may want to abstract away a bit more.

Use a Class to create public values

The third example is utilizing a static class. Within the class you can define a public static variable, that is available when importing in the class to any of your playwright files. This approach is really nice and scalable, as you can also add multiple static data information or environment variables while having access to the nice intellisense type ahead!

// lib/helpers/staticVariables.ts

export class StaticVariables {
  public static staticApiURL = process.env.API_URL;
}

The below 3 examples can be seen below, within a datafactory file called login.ts. This datafactory function getLoginToken() was written to be able to make an API call from within a UI test with any email/password combination and get an authentication token, which I can then save to session storage to authenticate with the Angular UI application.

// lib/datafactory/login.ts

import { expect, request } from "@playwright/test";
import { StaticVariables } from "../helpers/staticVariables";

let apiURL;

// hardcoding the url
apiURL = "https://api.practicesoftwaretesting.com";

// using env variables directly
apiURL = process.env.API_URL;

// using a dedicated class to access variables
apiURL = StaticVariables.staticApiURL;

export async function getLoginToken(email: string, password: string) {
  const createRequestContext = await request.newContext();
  const response = await createRequestContext.post(apiURL + "/users/login", {
    data: {
      email: email,
      password: password,
    },
  });

  expect(response.status()).toBe(200);

  const body = await response.json();
  return body.access_token;
}

Out of these 3 options my favorite approach is #3. This approach can be used across any typescript file which makes it very appealing to me as a good path forward.

Using Fixtures to create an apiURL

There are two approaches I'll provide below.

Approach 1 with page.ts fixture

The first is how I originally solved my problem. This approach creates a fixture to extend test adding an apiURL type string to the Test Options. By default if no apiURL is provided an empty string will be assigned. I've added the below example to the lib/pages.ts file, this is a file that I use for my base page object so I don't have to import every single file into my UI tests.

// lib/pages.ts

import { test as base } from "@playwright/test";

export * from "./pages/loginPage";
export * from "./pages/homePage";
export * from "./pages/checkoutPage";

export type TestOptions = {
  apiURL: string;
};

// This will allow you to set apiURL in playwright.config.ts
export const test = base.extend<TestOptions>({
  apiURL: ["", { option: true }],
});

export default test;

To utilize this you must import test from lib/pages within your playwright test and have an apiURL set within the playwright.config.ts (note I have apiURL which we are using for this example and for the next section ware using apiBaseURL. I did this mainly so I could see how both fixtures worked in parallel.

// playwright.config.ts

import { defineConfig } from "@playwright/test";
import type { APIRequestOptions } from "./lib/fixtures/apiRequest";
import { TestOptions } from "./lib/pages";

require("dotenv").config();

export default defineConfig<APIRequestOptions & TestOptions>({
  testDir: "./tests",
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [["html"], ["list"]],
  use: {
    baseURL: process.env.UI_URL,
    apiURL: process.env.API_URL,
    apiBaseURL: process.env.API_URL,
    trace: "retain-on-failure",
  },
});

In the below test file, notice we are importing test from "../lib/pages" this gives us access to use the apiURL in the beforeEach block that was set in the playwright.config.ts.

// tests/checkoutWithPageFixture.spec.ts

import { expect } from "@playwright/test";
import { test, CheckoutPage, HomePage } from "../lib/pages";

test.describe("Basic UI Checks With Page Fixture", () => {
  const username = process.env.USERNAME || "";
  const password = process.env.PASSWORD || "";

  test.beforeEach(async ({ page, request, apiURL }) => {
    // Gets Login Token via API call using apiBaseURL from fixture
    const response = await request.post(apiURL + "/users/login", {
      data: {
        email: username,
        password: password,
      },
    });

    expect(response.status()).toBe(200);

    const body = await response.json();
    const token = body.access_token;

    // Sets Local Storage with Login token so user is logged in
    await page.addInitScript((value) => {
      window.localStorage.setItem("auth-token", value);
    }, token);
  });

  test("Add to Cart and Checkout", async ({ page }) => {
    const homePage = new HomePage(page);
    const checkoutPage = new CheckoutPage(page);

    await homePage.goto();

    await homePage.product2.click();
    await homePage.addToCart.click();
    await homePage.navCart.click();

    await checkoutPage.proceed1.click();
    await checkoutPage.proceed2.click();
    await checkoutPage.address.fill("123 test street");
    await checkoutPage.city.fill("testville");
    await checkoutPage.state.fill("test");
    await checkoutPage.country.fill("united states");
    await checkoutPage.postcode.fill("12345");

    await checkoutPage.proceed3.click();
    await checkoutPage.paymentMethod.selectOption("2: Cash on Delivery");

    await checkoutPage.accountName.fill("testy");
    await checkoutPage.accountNumber.fill("1234124");
    await checkoutPage.finish.click();

    await expect(checkoutPage.success.first()).toBeVisible();
  });
});

Approach 2 with apiRequests.ts fixture

I take 0 credit for this approach is it was provided by  Yury Semikhatsky when providing feedback on a feature request to add an api endpoint baseURL (if interested go add a 👍 to the request).

The approach is very similar to fixture approach 1 but Yury takes it to the next level, by not only creating an apiBaseURL TestOption that can be imported from the playwright.config.ts file (same file as above) but also extending test with apiRequest which when called will utilize the apiBaseURL by default replacing the baseURL. This is super slick, but may not be straight forward if you work with more junior level folks in a project.

// lib/fixtures/apiReqeusts.ts

import { test as base, APIRequestContext, request } from "@playwright/test";

export type APIRequestOptions = {
  apiBaseURL: string;
};

type APIRequestFixture = {
  apiRequest: APIRequestContext;
};

// This fixture will override baseURL with apiBaseURL from playwright.config.ts whenever it is used
export const test = base.extend<APIRequestOptions & APIRequestFixture>({
  apiBaseURL: ["", { option: true }],

  apiRequest: async ({ apiBaseURL }, use) => {
    const apiRequestContext = await request.newContext({
      baseURL: apiBaseURL,
    });

    await use(apiRequestContext);
    await apiRequestContext.dispose();
  },
});

The implementation within a test spec, requires you to import test from the ../lib/fixtures/apiRequest file and when making an api call with apiRequest you don't even need to pass in a baseURL, it will automatically replace the baseURL -> apiBaseURL from the playwright.config.ts

// tests/checkoutWithApiFixture.spec.ts

import { expect } from "@playwright/test";
import { test } from "../lib/fixtures/apiRequest";
import { CheckoutPage, HomePage } from "../lib/pages";

test.describe("Basic UI Checks With API Fixture", () => {
  const username = process.env.USERNAME || "";
  const password = process.env.PASSWORD || "";

  test.beforeEach(async ({ page, apiRequest }) => {
    // Gets Login Token via API call using apiBaseURL from fixture but all within the fixture so you don't event need to add apiURL to the test
    const response = await apiRequest.post("/users/login", {
      data: {
        email: username,
        password: password,
      },
    });

    expect(response.status()).toBe(200);

    const body = await response.json();
    const token = body.access_token;

    // Sets Local Storage with Login token so user is logged in
    await page.addInitScript((value) => {
      window.localStorage.setItem("auth-token", value);
    }, token);
  });

  test("Add to Cart and Checkout", async ({ page }) => {
    const homePage = new HomePage(page);
    const checkoutPage = new CheckoutPage(page);

    await homePage.goto();

    await homePage.product2.click();
    await homePage.addToCart.click();
    await homePage.navCart.click();

    await checkoutPage.proceed1.click();
    await checkoutPage.proceed2.click();
    await checkoutPage.address.fill("123 test street");
    await checkoutPage.city.fill("testville");
    await checkoutPage.state.fill("test");
    await checkoutPage.country.fill("united states");
    await checkoutPage.postcode.fill("12345");

    await checkoutPage.proceed3.click();
    await checkoutPage.paymentMethod.selectOption("2: Cash on Delivery");

    await checkoutPage.accountName.fill("testy");
    await checkoutPage.accountNumber.fill("1234124");
    await checkoutPage.finish.click();

    await expect(checkoutPage.success.first()).toBeVisible();
  });
});


Yury's codebase/example can be found here:

GitHub - yury-s/bug-23738: bug-23738
bug-23738. Contribute to yury-s/bug-23738 development by creating an account on GitHub.

All the code examples  I have in this article can be found within this Repository.

GitHub - playwrightsolutions/playwright-practicesoftwaretesting.com: Example using Playwright against site https://practicesoftwaretesting.com
Example using Playwright against site https://practicesoftwaretesting.com - GitHub - playwrightsolutions/playwright-practicesoftwaretesting.com: Example using Playwright against site https://practi...
Front End / Back End Meme

To wrap things up, there are lots of ways to solve this problem, and I am sure there are more I hadn't even covered, but hopefully one of these approaches will make your playwright tests a bit more clean and maintainable, if you would like first class support for this feature baked into playwright add an upvote to this feature request!

[Feature]: add apiEndpoint to playwright.config.ts (similar to baseUrl) · Issue #23738 · microsoft/playwright
Let us know what functionality you’d like to see in Playwright and what your use case is. Do you think others might benefit from this as well? Yes From time to time, we like to test our API as well...

Thanks for reading! If you found this helpful, reach out and let me know on LinkedIn or consider buying me a cup of coffee. If you want more content delivered to you in your inbox subscribe below, and be sure to leave a ❤️ to show some love.