Scalable Integration Testing with Playwright, Docker, and GitHub Actions
Introduction
In modern software development, ensuring that your web application works correctly across all its components is critical. Integration testing allows you to verify that the various parts of your application, such as the backend APIs, databases, and frontend, work together as expected. By using Docker, Playwright, Jest, and GitHub Actions, you can automate your integration tests in a way that is both scalable and reliable.
In this post, we’ll walk through the setup of integration tests for your web application using these technologies. We will containerize the environment using Docker, write automated tests with Playwright and Jest, and then run those tests in a CI pipeline using GitHub Actions.
Why Use Docker for Integration Testing?
Docker is a platform that allows you to package applications and all their dependencies into containers. Containers are isolated, lightweight, and portable, meaning you can run them consistently across different environments. For integration testing, Docker is invaluable because it allows you to simulate an entire web app environment — including databases, APIs, and frontend services — without worrying about external system dependencies.
By using Docker Compose, you can manage multiple containers that represent different parts of your application. This gives you a realistic environment for testing how different services interact.
Key Benefits of Docker in Testing:
- Environment Consistency: Docker ensures that your tests run in the same environment on every machine — be it your local machine, staging server, or CI pipeline.
- Isolation: Each service (like the database or API) runs in its own container, so they don’t interfere with each other.
- Scalability: You can scale your services up or down based on the needs of your tests.
Step 1: Setting Up Docker with Docker Compose
Docker Compose allows you to define and run multi-container Docker applications using a single configuration file (docker-compose.yml). This is especially useful when you need to run multiple services (like a database and an app server) together.
Let’s start by creating a docker-compose.yml file that sets up two services:
- PostgreSQL Database: A database that our application will interact with.
- App (API): The app itself, which depends on the database.
docker-compose.yml:
version: '3.8'
services:
# PostgreSQL Database Service
postgres:
image: postgres:13
environment:
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpassword
POSTGRES_DB: testdb
ports:
- "5432:5432" # Exposing port for app to connect
networks:
- test-network # Network for communication between containers
# App Service (Your backend API)
app:
build: . # The app is built from a Dockerfile in the current directory
environment:
DATABASE_URL: postgres://testuser:testpassword@postgres:5432/testdb
depends_on:
- postgres # Ensure the database is up before the app starts
networks:
- test-network
ports:
- "4000:4000" # Expose app's port
networks:
test-network:
driver: bridgyamlExplanation of docker-compose.yml:
PostgreSQL Service (postgres):
- We pull the official PostgreSQL 13 image.
- We define the environment variables to set up the database username, password, and database name.
- The
portssection ensures that the database is accessible at port5432from other containers and your host machine. - The
networkssection ensures that both the PostgreSQL and the app services can communicate with each other on the same isolated network.
App Service (app):
- The app is built from the local Dockerfile.
- The
DATABASE_URLenvironment variable tells the app where to connect to the database. This will point to the PostgreSQL container (postgres). - We use
depends_onto make sure the app service waits until PostgreSQL is up and running before starting. - The app is exposed on port
4000, so we can test it.
Networks: Both services are part of the same network (test-network), which allows them to communicate with each other.
Why Use Docker Compose?
- Multi-Service Setup: Docker Compose makes it easy to spin up both your database and your app together with a single command.
- Easy Configuration: The
docker-compose.ymlfile is simple to write, and you can define your environment variables and networking options in one place. - Replicability: You can share the
docker-compose.ymlfile across environments (local, CI, etc.), ensuring consistency.
Step 2: Setting Up Playwright and Jest
Now that we’ve set up our Dockerized environment, it’s time to write some tests.
Installing Dependencies:
We need Playwright for browser automation and Jest for testing the backend APIs.
Run the following commands to install the dependencies:
npm install playwright jest jest-playwright-preset node-fetch- Playwright: A browser automation library for running tests in real browsers.
- Jest: A testing framework to run our API tests.
- jest-playwright-preset: A Jest preset that makes it easy to integrate Playwright with Jest.
Playwright Test (Frontend)
Let’s write a Playwright test to verify that the homepage of the app loads correctly. Create a file under tests/integration/playwright.test.js:
// tests/integration/playwright.test.js
const { test, expect } = require('@playwright/test');
test('should load the home page', async ({ page }) => {
await page.goto('http://app:4000'); // App is exposed on port 4000
const title = await page.title();
expect(title).toBe('Home Page');
});Explanation of Playwright Test:
page.goto(): This command tells Playwright to visit the homepage of your app, which is running inside the Docker container (http://app:4000).expect(title).toBe('Home Page'): After navigating to the page, we check if the title of the page matches the expected value'Home Page'.
Jest Test (Backend)
Next, we’ll write a Jest test to check if the backend API is working. Create the file tests/integration/api.test.js:
// tests/integration/api.test.js
const fetch = require('node-fetch');
describe('API Tests', () => {
it('should return a 200 status for /api/users', async () => {
const response = await fetch('http://app:4000/api/users');
expect(response.status).toBe(200); // Check if API responds with a 200 OK status
});
it('should create a user in the database', async () => {
const response = await fetch('http://app:4000/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'John Doe' }),
});
const data = await response.json();
expect(data.name).toBe('John Doe'); // Verify if the created user’s name is correct
});
});Explanation of Jest Test:
fetch: We usenode-fetchto make HTTP requests to the app’s API.- GET
/api/users: This test sends a GET request to the/api/usersendpoint and verifies that the API returns a200status code. - POST
/api/users: This test sends a POST request to create a new user. It checks if the response data matches the input, confirming that the user was successfully created in the database.
Step 3: CI Pipeline with GitHub Actions
To automate the testing process, we’ll set up GitHub Actions. This will allow the tests to run automatically when changes are made to the codebase or on a scheduled basis.
Create a .github/workflows/integration-tests.yml file to define the pipeline:
name: Run Integration Tests in Docker
on:
schedule:
- cron: '0 0 * * *' # Runs tests every day at midnight UTC
workflow_dispatch: # Allows manual triggering of the workflow
jobs:
setup-and-test:
runs-on: ubuntu-latest
services:
docker:
image: docker:19.03.12
options: --privileged
ports:
- 5432:5432 # Expose database port for testing
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker
run: |
docker-compose -f docker-compose.yml up --build -d # Start the containers
- name: Run Playwright Tests
run: |
npx playwright test tests/integration/playwright.test.js # Run frontend tests
- name: Run Jest API Tests
run: |
npx jest tests/integration/api.test.js # Run backend API tests
- name: Clean up Docker
run: |
docker-compose down # Tear down the containers after testsExplanation of GitHub Actions Workflow:
on.schedule: The tests will run every day at midnight UTC, but you can modify the cron expression to suit your needs.- Docker Setup: The workflow sets up Docker, builds the containers from the
docker-compose.ymlfile, and runs them in detached mode. - Run Playwright Tests: The frontend tests (Playwright) are run against the application inside the container.
- Run Jest Tests: The backend API tests (Jest) are run against the API.
- Clean Up: After the tests are complete, Docker containers are brought down to free up resources.
Conclusion
With Docker, Playwright, Jest, and GitHub Actions, you can set up a robust and scalable integration testing pipeline. This approach is reliable, as it isolates the testing environment and ensures consistent results across different stages of development.
By containerizing your application and running your tests in isolated, reproducible environments, you ensure that your tests are scalable, maintainable, and easy to set up. This also makes it simple to extend the setup in the future by adding more services, scaling the environment, or parallelizing tests for even faster feedback.
As your application grows, this solution can easily be adapted to meet new requirements, making it an ideal choice for modern software development teams.
