Embedding Drift in Your Test Framework
If you already have a test framework (Jest, Playwright, JUnit, pytest, etc.), you can embed Drift as a test case within it. This allows you to:
Run Drift tests alongside unit/integration tests in your CI/CD pipeline.
Use your framework's reporting (JUnit, HTML, etc.).
Access mocks and stubs from your existing test infrastructure.
Reuse test databases and fixtures.
Combine with state management from the previous tutorial.
The Architecture
┌──────────────────────────────┐
│ Your Test Framework │
│ (Jest, Playwright, etc) │
└──────────┬───────────────────┘
│
│ spawns process
│
▼
┌──────────────────────────────┐
│ Drift Verifier │
│ (CLI subprocess) │
└──────────┬───────────────────┘
│
│ validates
│
▼
┌──────────────────────────────┐
│ Your API Server │
│ (Express, FastAPI, etc) │
└──────────────────────────────┘
│
│ uses
│
▼
┌──────────────────────────────┐
│ Shared Resources │
│ - Database │
│ - Mocks/Stubs │
│ - Test Fixtures │
└──────────────────────────────┘Scenario | Benefit |
|---|---|
You have Jest/pytest tests | Run Drift with same setup/teardown as other tests |
Need mocks/stubs for APIs | Access your mock server from Drift tests |
Share database fixtures | Use same test data as unit tests |
Report in one place | Combine API contract tests with other tests |
Run in CI/CD | Single test command runs everything |
Create a Drift Wrapper
Create a utility function that spawns Drift as a subprocess and captures the exit code.
Node.js/Jest Example
Create automation/drift.js:
const { spawn } = require('child_process');
const path = require('path');
const runDrift = (options = {}) => {
const {
testFile = './drift.yaml',
serverUrl = 'http://localhost:8080',
outputDir = './output',
logLevel = 'info'
} = options;
return new Promise((resolve, reject) => {
console.log(`\n📋 Running Drift tests from: ${testFile}\n`);
const child = spawn('drift', [
'verifier',
'--test-files', testFile,
'--server-url', serverUrl,
'--log-level', logLevel,
'--output-dir', outputDir
], {
stdio: 'inherit', // Shows Drift output in test output
shell: true
});
child.on('error', (err) => {
reject(new Error(`Failed to run Drift: ${err.message}`));
});
child.on('close', (code) => {
resolve(code ?? 1);
});
});
};
module.exports = { runDrift };Key options:
stdio: 'inherit'- Displays Drift output directly in your test runner.Exit code
0= all tests passed, non-zero = failures.
Python/pytest Example
Create automation/drift.py:
import subprocess
import sys
def run_drift(test_file='./drift.yaml', server_url='http://localhost:8080',
output_dir='./output', log_level='info'):
"""Run Drift verifier and return exit code"""
print(f"\n📋 Running Drift tests from: {test_file}\n")
result = subprocess.run([
'drift',
'verifier',
'--test-files', test_file,
'--server-url', server_url,
'--log-level', log_level,
'--output-dir', output_dir
])
return result.returncodeCreate automation/drift.py:Start Your API Server in the Test
Create a Jest test that starts your API server, then runs Drift.
Jest Example
Create tests/api.test.js:
const express = require('express');
const { runDrift } = require('../automation/drift');
// Use in-memory database for tests (no cleanup needed)
process.env.REPOSITORY_TYPE = 'inmemory';
describe('API Contract Tests', () => {
let server;
// Setup: Start the API server before tests
beforeAll(async () => {
const app = express();
// Mount your API routes
app.use(require('../src/product/routes'));
app.use(require('../automation/test-routes')); // State mgmt routes
// Start server on port 8080
server = app.listen(8080);
console.log('✓ API server started on http://localhost:8080');
});
// Cleanup: Stop the server after tests
afterAll(async () => {
return new Promise((resolve) => {
server.close(resolve);
});
});
// The actual test: Run Drift
it('Validates API conforms to OpenAPI specification', async () => {
const exitCode = await runDrift({
testFile: './drift.yaml',
serverUrl: 'http://localhost:8080',
logLevel: 'debug'
});
// Drift exit code 0 = all tests passed
expect(exitCode).toBe(0);
});
});pytest Example
Create tests/test_api_contract.py:
import subprocess
import pytest
from your_app import create_app
@pytest.fixture(scope='module')
def api_server():
"""Start API server for the test module"""
app = create_app(config='testing')
app.config['REPOSITORY_TYPE'] = 'inmemory'
# Run in debug mode
server = app.run(port=8080, debug=False)
yield server
server.shutdown()
def test_api_conforms_to_openapi(api_server):
"""Drift validates the API against its OpenAPI spec"""
from automation.drift import run_drift
exit_code = run_drift(
test_file='./drift.yaml',
server_url='http://localhost:8080',
log_level='debug'
)
assert exit_code == 0, "Drift tests failed"Combine with State Management
The Drift tests can use the state management pattern from the managing state tutorial.
Your drift.yaml stays the same:
# yaml-language-server: $schema=https://download.pactflow.io/drift/schemas/drift.testcases.v1.schema.json
drift-testcase-file: v1
title: "Product API Tests"
sources:
- name: source-oas
path: ./openapi.yaml
- name: state-mgmt
path: ./state-management.lua
plugins:
- name: oas
- name: json
global:
auth:
apply: true
parameters:
authentication:
scheme: bearer
token: ${state-mgmt:bearer_token}
operations:
getProductByID_Success:
target: source-oas:getProductByID
parameters:
path:
id: 10
expected:
response:
statusCode: 200The state-management.lua script (from the previous tutorial) handles setup/teardown automatically.
Run Your Tests
# Run all tests (unit + Drift contract tests) npm test # Run only API contract tests npm test -- api.test.js # Run with specific log level LOG_LEVEL=debug npm test
Output:
PASS tests/api.test.js
API Contract Tests
✓ Validates API conforms to OpenAPI specification (2345ms)
Running Drift tests from: ./drift.yaml
✓ getProductByID_Success
✓ getProductByID_NotFound
✓ createProduct_Success
✓ deleteProduct_Success
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 totalAdvanced: Multiple Test Variants
You can run Drift against different configurations:
describe('API Contract Tests', () => {
// Test with in-memory database
it('Works with in-memory database', async () => {
process.env.REPOSITORY_TYPE = 'inmemory';
const exitCode = await runDrift();
expect(exitCode).toBe(0);
});
// Test with PostgreSQL
it('Works with PostgreSQL', async () => {
process.env.REPOSITORY_TYPE = 'postgres';
process.env.DATABASE_URL = 'postgres://test:test@localhost/testdb';
const exitCode = await runDrift({
testFile: './drift-postgres.yaml'
});
expect(exitCode).toBe(0);
});
// Test specific tags only
it('Smoke tests pass', async () => {
const exitCode = await runDrift({
// Use --tags flag in drift command
testFile: './drift.yaml'
// Would need to enhance runDrift() to support --tags
});
expect(exitCode).toBe(0);
});
});Combining with Existing Test Setup
If you have existing test setup (fixtures, database seeding, etc.), Drift reuses it:
describe('Product API', () => {
let database;
let server;
beforeAll(async () => {
// Your existing setup
database = await setupTestDatabase();
await seedTestData(database);
server = startServer(database);
});
// Your existing tests
it('Creates a product', async () => {
const response = await fetch('http://localhost:8080/products', {
method: 'POST',
body: JSON.stringify({ name: 'Test' })
});
expect(response.status).toBe(201);
});
// Drift test - uses same database and server
it('Validates API contract with Drift', async () => {
const exitCode = await runDrift();
expect(exitCode).toBe(0);
});
afterAll(async () => {
server.close();
await database.close();
});
});Running in CI/CD
In your CI pipeline, Drift runs like any other test:
GitHub Actions
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 10s
steps:
- uses: actions/checkout@v3
- name: Install Drift
run: |
wget -O - https://download.pactflow.io/drift/latest/linux-x86_64.tgz | tar xz -C /usr/local/bin
drift --version
- name: Install dependencies
run: npm install
- name: Run all tests (including Drift)
run: npm test
env:
NODE_ENV: test
DATABASE_URL: postgres://postgres:test@localhost/testdbGitLab CI
test:
image: node:20
services:
- postgres:15
script:
- wget -O - https://download.pactflow.io/drift/latest/linux-x86_64.tgz | tar xz -C /usr/local/bin
- drift --version
- npm install
- npm test
variables:
POSTGRES_DB: testdb
POSTGRES_PASSWORD: testThis approach provides a single test runner with shared infrastructure, unified reporting, faster feedback on API contract issues, simpler maintenance through one repository and deployment flow, and a better developer experience with a single npm test command for all tests.
Troubleshooting
Check the most common issues.
Drift can't find the API
Ensure the server is started before Drift tests run:
beforeAll(async () => {
server = app.listen(8080);
// Give server time to start
await new Promise(resolve => setTimeout(resolve, 100));
});Ensure the server is started before Drift tests run:Port already in use
Check if another process is using port 8080:
lsof -i :8080 # Kill if needed: kill -9 <PID>
Drift not on PATH
Make sure Drift is installed and in your PATH:
which drift # If not found, install or add to PATH export PATH="/path/to/drift:$PATH"
Different behavior in CI vs local
Ensure environment variables are the same:
console.log('NODE_ENV:', process.env.NODE_ENV);
console.log('REPOSITORY_TYPE:', process.env.REPOSITORY_TYPE);Complete Working Example
The full implementation is available in the example repository. View the complete example project.
This includes:
Complete Jest test setup
Drift wrapper utility
State management integration
CI/CD configuration
Multiple database variants
Next Steps
Lifecycle Hooks. Learn about event handlers in Drift.
CI/CD Integration. Deploy Drift tests in your pipeline.
Debugging. Troubleshoot failing tests.
Testing with State Dependencies. The previous tutorial on state management.