Setting up a Walking Skeleton

End-to-end Testing with Playwright


Learning Objectives

  • You know how to add Playwright to a project for end-to-end testing.
  • You know how to run Playwright tests using Docker.

At this point, we have a walking skeleton that provides an user interface that interacts with a database through a server-side application. Next, we add end-to-end tests to the project using Playwright.

Setting up Playwright

Next, as the last step for the walking skeleton, we’ll add end-to-end tests to the project. End-to-end tests are used to test the whole application, including the user interface, the server-side application, and the database.

We use Playwright, which is a tool for automating browsers and testing web applications.

Inside the walking-skeleton folder in a terminal, run the command deno run -A npm:create-playwright@latest e2e-tests in the root folder of the project. The command creates a folder called e2e-tests and initiates a project for running Playwright tests. The command asks for a few options — use the following:

If the command does not work as expected, skip to the section “Creating the e2e-tests folder manually”.

  • Pick “JavaScript when asked whether you want to use TypeScript or JavaScript.
  • Pick “tests” when asked where to put the end-to-end tests.
  • Pick “false” when asked whether to add a GitHub Actions workflow.
  • Pick “false” when asked whether to install Playwright browsers.
  • Pick “false” when asked whether to install Playwright operating system dependencies.

Once done, the project has a new folder e2e-tests with the following contents (omitting node_modules).

cd e2e-tests
tree --dirsfirst
.
├── node_modules
│   └── ...
├── tests
│   └── example.spec.js
├── tests-examples
│   └── demo-todo-app.spec.js
├── package.json
├── package-lock.json
└── playwright.config.js
Loading Exercise...

Creating the e2e-tests folder manually

If the command deno run -A npm:create-playwright@latest e2e-tests does not work as expected, you can create the folder manually. To do this, create a folder e2e-tests in the root folder of the project and do the following steps:

  • Create a folder tests under the folder e2e-tests, and create a file called example.spec.js to the folder. Copy the following contents to the file.
// @ts-check
const { test, expect } = require('@playwright/test');

test('has title', async ({ page }) => {
  await page.goto('https://playwright.dev/');

  // Expect a title "to contain" a substring.
  await expect(page).toHaveTitle(/Playwright/);
});
  • Create a file called playwright.config.js to the folder e2e-tests. Copy the text “TODO” to the file. The actual contents needed will be shown in the section “Cleaning up the Playwright configuration” in a second.

  • Create a file called package.json to the folder e2e-tests and copy the following contents to the file.

{
  "name": "e2e-tests",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {},
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "devDependencies": {
    "@playwright/test": "^1.54.2",
    "@types/node": "^24.2.0"
  }
}

Remember to save the files!

Once ready, run the command deno install --allow-scripts in the folder. Now, you should be able to proceed.

Cleaning up the Playwright configuration

The playwright.config.js provides the configuration for Playwright. For our purpose, the configuration is a bit excessive, so let’s cut it down. Modify the configuration to match the following.

import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  testDir: "./tests",
  reporter: "list",
  use: {
    baseURL: "http://localhost:5173",
    trace: "off",
  },

  projects: [
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"] },
    },
  ],
});

In effect, we declare the directory for the tests and ask for the test results to be printed in a list format. Furthermore, we use http://localhost:5173 for the starting point for the tests, do not collect test traces, and run the tests using Chromium.

We’ll be running the tests with Docker and will download the necessary components there.

Containerizing Playwright

To run the tests with Docker, we need to create a Dockerfile for Playwright.

Before starting, remove the tests-examples and node_modules folders from the e2e-tests folder, and remove also the package-lock.json file. If you created the e2e-tests folder manually, these folders / files do not exist.

Create a file called Dockerfile into the folder e2e-tests with the following contents.

FROM mcr.microsoft.com/playwright:v1.54.2-jammy

WORKDIR /app

COPY package*.json .
COPY *config.js .

RUN npm install
RUN npx playwright install chromium

COPY . .

CMD [ "npx", "playwright", "test" ]

The above configuration uses the image mcr.microsoft.com/playwright:v1.54.2-jammy as the base image, creates a folder /app to the image and copies the configuration files to the folder. Then, we install the dependencies and the Chromium browser, which are used to run the tests. This is followed by copying the rest of the contents (i.e., the tests) to the image and finally, at the last line, the image is configured to run the tests using the command npx playwright test.

Next, we wish to add Playwright to the Docker Compose configuration to allow us to run the tests using Docker Compose. To do this, we add the following service to the compose.yml file.

  e2e-tests:
    entrypoint: "/bin/true"
    build: e2e-tests
    network_mode: host
    depends_on:
      - client
    volumes:
      - ./e2e-tests/tests:/app/tests

The above configuration creates a service called e2e-tests that uses the Dockerfile we just created. The entrypoint is used to indicate that we do not wish to run the tests by default (i.e., whenever we run docker compose up). The service is configured to use the host network, which allows the service to access applications running on the host machine. The service depends on the client service, which is the user interface of the application. Finally, the service is configured to map the tests in the folder e2e-tests/tests to the container.

At this point, the whole compose.yaml file is as follows.

services:
  client:
    build: client
    restart: unless-stopped
    volumes:
      - ./client/src:/app/src
    ports:
      - 5173:5173
    depends_on:
      - server

  server:
    build: server
    restart: unless-stopped
    volumes:
      - ./server:/app
    ports:
      - 8000:8000
    env_file:
      - project.env
    depends_on:
      - database

  database:
    container_name: postgresql_database
    image: postgres:17.5
    restart: unless-stopped
    env_file:
      - project.env

  database-migrations:
    image: flyway/flyway:11.10
    env_file:
      - project.env
    depends_on:
      - database
    volumes:
      - ./database-migrations:/flyway/db/migration
    command: -connectRetries=60 -baselineOnMigrate=true migrate

  e2e-tests:
    entrypoint: "/bin/true"
    build: e2e-tests
    network_mode: host
    depends_on:
      - client
    volumes:
      - ./e2e-tests/tests:/app/tests

With the adjustments to the configuration, run the command docker compose up --build to start the services and to create the Playwright image — this takes a while on the first.

Loading Exercise...

Running Playwright tests

With the project running, we can run the tests.

The tests are run using the command docker compose run --rm --entrypoint=npx e2e-tests playwright test in the root folder of the project. Note! Keep the project running in a separate terminal window.

The command runs the tests and prints the results in the terminal, removing a created test container after it has finished.

docker compose run --rm --entrypoint=npx e2e-tests playwright test
[+] Creating 2/0
 ✔ Container wsd-walking-skeleton-api-1     Running
 ✔ Container wsd-walking-skeleton-client-1  Running

Running 2 tests using 1 worker

  ✓  1 [chromium] › example.spec.js:4:1 › has title (879ms)
  ✓  2 [chromium] › example.spec.js:11:1 › get started link (1.1s)

  2 passed (2.8s)

If you created the e2e-tests folder manually, there’s just one test instead of two.

Loading Exercise...

Testing our application

The tests that come with Playwright by default test the Playwright website. Rename the test file example.spec.js in the folder tests of the Playwright project to todos.spec.js and modify the contents of the file to match the following.

import { expect, test } from "@playwright/test";

test('Page at / has heading "Todos".', async ({ page }) => {
  await page.goto("/");
  await expect(page.getByText("Todos")).toBeVisible();
});

Now, when you run the tests, you see the following output.

$ docker compose run --rm --entrypoint=npx e2e-tests playwright test
(...)

Running 1 test using 1 worker

  ✓  1 [chromium] › tests/todos.spec.js:3:5 › Page at / has heading "Todos". (334ms)

  1 passed (2.3s)

The simple test passes, yay!

Summary

In summary:

  • End-to-end tests are used to test the whole application, including the user interface, the server-side application, and the database. Playwright is a tool for automating browsers and testing web applications.
  • We added Playwright to the project for end-to-end testing.
  • We created a Dockerfile for Playwright and added Playwright to the Docker Compose configuration.
  • We ran the Playwright tests using Docker Compose.
  • We created a simple test that checks that the user interface has the heading “Todos”.

At this point, the walking skeleton should have four folders, client, server, database-migrations, and e2e-tests, and two files, compose.yaml and project.env. The structure of the project should be similar to the following (the contents of almost all of the folders are omitted).

$ tree --dirsfirst
.
├── client
│   └── ...
├── database-migrations
│   └── ...
├── e2e-tests
│   ├── tests
│   │   └── todos.spec.js
│   └── ...
├── server
│   └── ...
├── compose.yaml
└── project.env

Now, you have a walking skeleton that includes a client-side application, a server-side application, a database, and end-to-end tests. Use this as the foundation for the rest of the course!

Loading Exercise...