iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🎭

Playwright + GitHub Actions: Insights on E2E and VRT Environment Setup and CI/CD Integration

に公開
2024/08/01

Introduction

Since I was in charge of Playwright environment setup and CI/CD integration for my work, I had been constantly thinking about the best configuration for E2E testing and VRT.
I have managed to refine it into a form that I am reasonably satisfied with, so I will document that knowledge here.

🎭 Playwright

A testing tool developed by Microsoft. It performs E2E testing with features like multi-browser support, auto-waiting, and parallel processing. It can also detect visual changes through screenshot comparison tests (VRT: Visual Regression Testing). It seems to be developed by the team that originally created Puppeteer.

https://playwright.dev/

Cypress is a common comparison tool, but recently, Playwright's weekly downloads seem to have surpassed it.
(Others include Selenium and paid E2E testing tools provided by vendors.)

Also, while Playwright officially supports Java, Python, and C#, we will be using TypeScript this time.

Requirements

First, within this repository, there are two packages: server (Express.js) and web (React.js), and the development environment is started using docker compose up. The DB is also defined in docker-compose.yml.

  1. E2E tests pass on GitHub Actions
  2. VRT passes on GitHub Actions
  3. Be able to reproduce the same results in both local and CI environments

In this article, E2E and VRT are defined as follows:

  • E2E (End-to-End Testing): Testing to confirm that functions work correctly throughout the entire system.
  • VRT (Visual Regression Testing): Testing to detect visual changes.

Source

Here is the source code for this project.

https://github.com/ishiyama0530/playwright-e2e-stack

This is the GitHub Actions execution history for this repository. E2E tests and VRT are executed daily at 9:00 AM.

https://github.com/ishiyama0530/playwright-e2e-stack/actions

Overall Configuration

/playwright-e2e-stack
├── /.github/workflows
│   ├── e2e.yml
│   └── vrt.yml
├── /e2e
│   ├── /tests # E2E test cases
│   ├── Dockerfile # for E2E testing environment
│   └── playwright.config.ts # E2E testing settings
├── /server
│   └── Dockerfile
├── /web
│   ├── /web/tests/visual/mock/db.json # mock data for json-server
│   ├── /tests # VRT test cases
│   ├── Dockerfile
│   ├── Dockerfile.test # for VRT execution environment
│   ├── playwright.config.ts # VRT settings
│   └── docker-compose.vrt.yml
├── docker-compose.yml
└── docker-compose.e2e.yml

Thinking behind the directory structure for E2E and VRT

  1. E2E Testing:
    Since these are tests performed across the entire system, the e2e directory is created at the same level as server and web. This makes it:

    • Clear that it is a system-wide test by placing it in the root directory.
    • Easier to design tests considering both the server and web.
  2. VRT:
    These tests detect visual changes in the UI and are specialized for the frontend. Therefore:

    • Placing test definitions under web makes the relationship with UI changes clear.
    • Easier for frontend developers to manage directly.
    • Using API mocks allows testing without depending on the backend.

E2E (End-to-End Testing) Details

Key Points

Execute E2E tests with docker compose -f docker-compose.e2e.yml up.

docker-compose.e2e.yml
services:
  db:
    image: postgres:latest
    environment:
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: password
      POSTGRES_DB: mydb
    ports:
      - "5432:5432"
    healthcheck:
      test: pg_isready -U admin -d mydb
      interval: 3s
      timeout: 3s
      retries: 5

  server:
    build:
      context: ./server
      dockerfile: Dockerfile
    environment:
      - DATABASE_URL=postgresql://admin:password@db:5432/mydb?schema=public
      - WEB_ORIGIN=http://web
    ports:
      - "3000:3000"
    command: >
      sh -c "npx prisma migrate deploy && npm start"
    depends_on:
      db:
        condition: service_healthy

  web:
    build:
      context: ./web
      dockerfile: Dockerfile
      args:
        - VITE_API_URL=http://server:3000
    ports:
      - "80:80"

  e2e:
    build:
      context: ./
      dockerfile: ./e2e/Dockerfile
    environment:
      - CI=${CI}
      - DATABASE_URL=postgresql://admin:password@db:5432/mydb?schema=public
      - PLAYWRIGHT_BASE_URL=http://web
      - PLAYWRIGHT_API_URL=http://server:3000
    volumes:
      - ./e2e/test-results:/app/test-results
    ports:
      - "9323:9323"
    depends_on:
      - web

The process follows this sequence:

  1. Starting the DB container

    • Starts the Postgres database.
    • Runs a health check to confirm that the database is ready.
  2. Starting the Server container

    • Waits for the DB container's health check to succeed.
    • Builds the server application.
    • Executes Prisma migrations.
    • Starts the server application.
  3. Starting the Web container

    • Builds the frontend application.
    • Starts the web server.
  4. Starting the E2E Test container

    • Builds the E2E testing environment.
    • Waits for the Web container to start.
    • Prepares for test execution.
  5. Executing tests

    • Executes Playwright tests inside the E2E test container.
    • Saves the test results to the directory mounted as a volume.
e2e/Dockerfile
FROM mcr.microsoft.com/playwright:latest

EXPOSE 9323
WORKDIR /app

# By running npm ci before copying the source code, we can cache the dependency layer
COPY ./e2e/package*.json .
RUN npm ci

COPY ./e2e .
COPY ./server/prisma ./prisma
RUN npx prisma generate

CMD ["npm", "test"]

For the test execution environment, we use Playwright's official "mcr.microsoft.com/playwright" as the base image. Adopting the official image allows us to skip the manual setup of dependencies required for test execution, significantly reducing the effort needed for environment construction.

Furthermore, by using a containerized testing environment, we can minimize the differences in the execution environment between local development and CI. This prevents inconsistencies in test results caused by environmental differences, leading to more reliable and consistent outcomes.

.github/workflows/e2e.yml
name: End-to-End Testing
run-name: E2E Testing By @${{ github.actor }}
on:
  workflow_dispatch:
  push:
    branches: ["main"]
  schedule:
    # Daily at 9:00 (JST)
    - cron: "0 0 * * *"

jobs:
  e2e:
    timeout-minutes: 10
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: docker compose build
        run: docker compose -f docker-compose.e2e.yml build

      - name: Run Playwright tests
        run: docker compose -f docker-compose.e2e.yml up --abort-on-container-exit --exit-code-from e2e

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: e2e/playwright-report/
          retention-days: 30

      - name: Cleanup Docker resources
        if: always()
        run: docker compose -f docker-compose.e2e.yml down --rmi all --volumes --remove-orphans

Here is an explanation of the GitHub Actions workflow:

  • Trigger Settings:
    For convenience, it's set to run on pushes to the main branch, but E2E tests often take more time than other tests. Therefore, it is generally recommended to run them on a schedule (daily) for specific branches.

  • Executing Tests:
    Tests are executed via docker compose -f docker-compose.e2e.yml up --abort-on-container-exit --exit-code-from e2e (same as in the local environment). The --abort-on-container-exit flag ensures that when any container exits, all other containers are stopped, and --exit-code-from e2e uses the exit code of the e2e container as the overall command's exit code.

  • Uploading Test Reports:
    The test reports created under e2e/playwright-report are uploaded as artifacts. This allows you to check the details of tests performed on GitHub Actions.

  • Cleaning Up Resources:
    docker compose down --rmi all --volumes --remove-orphans deletes related resources. Not leaving unnecessary data or settings helps mitigate potential security risks.

I borrowed the command from here.
https://qiita.com/suin/items/19d65e191b96a0079417

GitHub Actions Test Report

After running the tests on GitHub Actions, downloading the generated "playwright-report" and running npx playwright show-report [downloaded-path] allows you to check detailed test results and failure screenshots, helping you identify and solve issues.

npx playwright show-report ~/Downloads/
# or
npx playwright show-report ~/Downloads/playwright-report etc.

Also, instead of downloading the test results, it seems they can be hosted on GitHub Pages. That's a nice touch.

https://ysfaran.github.io/blog/2022/09/02/playwright-gh-action-gh-pages/#publish-html-report-to-github-pages

VRT (Visual Regression Testing) Details

Where to store the snapshots?

First, concerning where to store the captures used for image comparison, the following candidates can be considered:

  • Cloud storage such as S3
  • GitHub Actions Artifacts
  • Capturing a baseline for comparison during every execution
  • Including them in the commit

Reference
https://zenn.dev/cybozu_ept/articles/practice-vrt-using-github-actions-cache#vrt-導入の課題-〜テストオラクルたるスクリーンショットをどこに保管する?〜

I decided to include them in the commits because I wanted to use a format that is as independent of external factors as possible.

API Mock Server

For the API mock server, I am using json-server.

web/tests/visual/mock/db.json
{
    "users": [
        {
            "id": 1,
            "name": "John Doe",
            "email": "john.doe@example.com"
        },
        {
            "id": 2,
            "name": "Jane Smith",
            "email": "jane.smith@example.com"
        }
    ]
}

By preparing a JSON file like the one above, you can easily set up a convenient REST API.

Other Key Points

Similar to the E2E tests, the tests are executed with docker compose -f ./web/docker-compose.vrt.yml up.

web/docker-compose.vrt.yml
services:
  vrt:
    build:
      context: ./
      dockerfile: Dockerfile.test
      args:
        - VITE_API_URL=http://localhost:3000
    environment:
      - CI=${CI}
    ports:
      - "80:80"
      - "3000:3000"
      - "9323:9323"
    volumes:
      - ./playwright-report:/app/playwright-report
      - ./test-results:/app/test-results
      - ./snapshots:/app/snapshots
      - ./tests/:/app/tests

For VRT, only the web directory is used.
To reiterate, the focus of VRT is the frontend. (Although the philosophy might differ by project...) Therefore, I chose a configuration that is entirely self-contained within the web directory.

web/Dockerfile.test
FROM mcr.microsoft.com/playwright:latest

EXPOSE 80
WORKDIR /app

COPY package*.json .
RUN npm ci

ARG VITE_API_URL
ENV VITE_API_URL=$VITE_API_URL

COPY . .
RUN npm run build 

CMD ["npm", "test"]

I am using the official Playwright image for the same reasons as in E2E.
Especially in VRT, device differences between snapshots taken on (my local) Mac and those taken on GitHub Actions become very apparent. To avoid this, a containerized testing environment seems essential.

web/playwright.config.ts
  webServer: [
    {
      command: "npm start",
      url: 'http://localhost',
      reuseExistingServer: !process.env.CI,
    },
    {
      command: "npm run mock",
      url: 'http://localhost:3000',
      reuseExistingServer: !process.env.CI,
    },
  ],

While in E2E, server startups were entirely managed by docker-compose.yml, in VRT, I use the webServer field in playwright.config.ts to launch the necessary servers when the tests are executed.

.github/workflows/vrt.yml
name: Visual Regression Testing
run-name: VRT By @${{ github.actor }}
on:
  workflow_dispatch:
  push:
    branches: ["main"]
  schedule:
    # Daily at 9:00 (JST)
    - cron: "0 0 * * *"

jobs:
  vrt:
    timeout-minutes: 10
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: docker compose build
        run: docker compose -f ./web/docker-compose.vrt.yml build

      - name: Run Playwright tests
        run: docker compose -f ./web/docker-compose.vrt.yml up --abort-on-container-exit --exit-code-from vrt

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: web/playwright-report/
          retention-days: 30

      - name: Cleanup Docker resources
        if: always()
        run: docker compose -f ./web/docker-compose.vrt.yml down --rmi all --volumes --remove-orphans

The workflow is identical to E2E except for the directory and docker-compose.yml specifications.

Summary

In this article, I explained the implementation of E2E testing and VRT using Playwright. Here are the key points:

  1. Environment Construction:

    • Built a consistent testing environment for both local and CI environments using docker-compose.
    • Minimized environmental dependencies by using the official Playwright image.
  2. E2E Testing:

    • Targeted the entire system and achieved testing integrated with the actual backend.
    • Placed the e2e directory at the root to clarify that these are system-wide tests.
  3. VRT (Visual Regression Testing):

    • Specialized for the frontend to detect visual changes in the UI.
    • Enabled frontend-only testing by using a mock server.
  4. CI/CD Integration:

    • Realized automatic test execution and report generation using GitHub Actions.
    • Achieved continuous quality assurance by running tests daily against specific branches.

First, regarding VRT: initially, I tried to perform VRT and E2E tests on the same scope, and I struggled a lot with sharing environments. After reconsidering and organizing the concerns of each, I believe I've arrived at a reasonably simple configuration. (This was the second point where I stumbled.)
Also, I realized I had been using many parts of docker-compose "somehow" without fully understanding them, so I took this opportunity to relearn. Feel free to take a look if you'd like. (laughs)

https://zenn.dev/ishiyama/scraps/d91448f8d3d6bf

E2E testing is easy to introduce and cost-effective as it allows verification of the entire system. However, it also comes with high maintenance costs. Starting with E2E tests, I want to ensure that integration and unit tests are also thoroughly implemented without neglect!

Finally, a big thank you to everyone who gave me various pieces of advice!

Appendix

Obstacle 1

Initially, I considered a configuration where docker-compose only managed the server and db, while the web was started in the local environment (host machine).

In this case, communication from web to server works via localhost in the local environment, but it fails on GitHub Actions.

https://blog.ojisan.io/container-test-on-gha/

As described in the article, managing the web with docker-compose as well allows them to communicate with each other through the Docker Network.

Obstacle 2

Initially, I viewed VRT as a type of E2E and managed both in the e2e directory.
The shared docker-compose.yml and playwright.config.ts resulted in an implementation with very low readability and maintainability.
This time, I completely separated them by defining E2E under the e2e directory and VRT under the web directory.

There may be cases where VRT is performed End-to-End. In such cases, I recommend building separate environments for each rather than forcing commonization.

Obstacle 3

Depending on the project, there were cases where service startup order could not be fully controlled even when using depends_on or healthcheck in docker-compose. In such cases, you can control it by using wait-for-it or dockerize.

This is also introduced in the official documentation.
https://docs.docker.jp/compose/startup-order.html

Discussion