references/contract-testing-with-postman.md
# Contract Testing with Postman
## Contract Testing with Postman
```json
// postman-collection.json
{
"info": {
"name": "User API Contract Tests"
},
"item": [
{
"name": "Get User",
"request": {
"method": "GET",
"url": "{{baseUrl}}/users/{{userId}}"
},
"test": "
pm.test('Response status is 200', () => {
pm.response.to.have.status(200);
});
pm.test('Response matches schema', () => {
const schema = {
type: 'object',
required: ['id', 'email', 'name'],
properties: {
id: { type: 'string' },
email: { type: 'string', format: 'email' },
name: { type: 'string' },
age: { type: 'integer' }
}
};
pm.response.to.have.jsonSchema(schema);
});
pm.test('Email format is valid', () => {
const data = pm.response.json();
pm.expect(data.email).to.match(/^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$/);
});
"
}
]
}
```
references/json-schema-validation.md
# JSON Schema Validation
## JSON Schema Validation
```python
# tests/contract/test_schema_validation.py
import pytest
import jsonschema
from jsonschema import validate
import json
# Define schemas
USER_SCHEMA = {
"type": "object",
"required": ["id", "email", "name"],
"properties": {
"id": {"type": "string"},
"email": {"type": "string", "format": "email"},
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0, "maximum": 150},
"role": {"type": "string", "enum": ["user", "admin"]},
"createdAt": {"type": "string", "format": "date-time"},
},
"additionalProperties": False
}
ORDER_SCHEMA = {
"type": "object",
"required": ["id", "userId", "total", "status"],
"properties": {
"id": {"type": "string"},
"userId": {"type": "string"},
"total": {"type": "number", "minimum": 0},
"status": {
"type": "string",
"enum": ["pending", "paid", "shipped", "delivered", "cancelled"]
},
"items": {
"type": "array",
"items": {
"type": "object",
"required": ["productId", "quantity", "price"],
"properties": {
"productId": {"type": "string"},
"quantity": {"type": "integer", "minimum": 1},
"price": {"type": "number", "minimum": 0},
}
}
}
}
}
class TestAPIContracts:
def test_get_user_response_schema(self, api_client):
"""Validate user endpoint response against schema."""
response = api_client.get('/api/users/123')
assert response.status_code == 200
data = response.json()
# Validate against schema
validate(instance=data, schema=USER_SCHEMA)
def test_create_user_request_schema(self, api_client):
"""Validate create user request body."""
valid_user = {
"email": "test@example.com",
"name": "Test User",
"age": 30,
}
response = api_client.post('/api/users', json=valid_user)
assert response.status_code == 201
# Response should also match schema
validate(instance=response.json(), schema=USER_SCHEMA)
def test_invalid_request_rejected(self, api_client):
"""Invalid requests should be rejected."""
invalid_user = {
"email": "not-an-email",
"age": -5, # Invalid age
}
response = api_client.post('/api/users', json=invalid_user)
assert response.status_code == 400
def test_order_response_schema(self, api_client):
"""Validate order endpoint response."""
response = api_client.get('/api/orders/order-123')
assert response.status_code == 200
validate(instance=response.json(), schema=ORDER_SCHEMA)
def test_order_items_array_validation(self, api_client):
"""Validate nested array schema."""
order_data = {
"userId": "user-123",
"items": [
{"productId": "prod-1", "quantity": 2, "price": 29.99},
{"productId": "prod-2", "quantity": 1, "price": 49.99},
]
}
response = api_client.post('/api/orders', json=order_data)
assert response.status_code == 201
result = response.json()
validate(instance=result, schema=ORDER_SCHEMA)
```
references/openapi-schema-validation.md
# OpenAPI Schema Validation
## OpenAPI Schema Validation
```typescript
// tests/contract/openapi.test.ts
import request from "supertest";
import { app } from "../../src/app";
import OpenAPIValidator from "express-openapi-validator";
import fs from "fs";
import yaml from "js-yaml";
describe("OpenAPI Contract Validation", () => {
let validator;
beforeAll(() => {
const spec = yaml.load(fs.readFileSync("./openapi.yaml", "utf8"));
validator = OpenAPIValidator.middleware({
apiSpec: spec,
validateRequests: true,
validateResponses: true,
});
});
test("GET /users/:id matches schema", async () => {
const response = await request(app).get("/users/123").expect(200);
// Validate against OpenAPI schema
expect(response.body).toMatchObject({
id: expect.any(String),
email: expect.stringMatching(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/),
name: expect.any(String),
age: expect.any(Number),
createdAt: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T/),
});
});
test("POST /users validates request body", async () => {
const invalidUser = {
email: "invalid-email", // Should fail validation
name: "Test",
};
await request(app).post("/users").send(invalidUser).expect(400);
});
});
```
references/pact-broker-integration.md
# Pact Broker Integration
## Pact Broker Integration
```yaml
# .github/workflows/contract-tests.yml
name: Contract Tests
on: [push, pull_request]
jobs:
consumer-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npm run test:pact
- name: Publish Pacts
run: |
npx pact-broker publish ./pacts \
--consumer-app-version=${{ github.sha }} \
--broker-base-url=${{ secrets.PACT_BROKER_URL }} \
--broker-token=${{ secrets.PACT_BROKER_TOKEN }}
provider-tests:
runs-on: ubuntu-latest
needs: consumer-tests
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npm run test:pact:provider
- name: Can I Deploy?
run: |
npx pact-broker can-i-deploy \
--pacticipant=UserService \
--version=${{ github.sha }} \
--to-environment=production \
--broker-base-url=${{ secrets.PACT_BROKER_URL }} \
--broker-token=${{ secrets.PACT_BROKER_TOKEN }}
```
references/pact-for-consumer-driven-contracts.md
# Pact for Consumer-Driven Contracts
## Pact for Consumer-Driven Contracts
### Consumer Test (Jest/Pact)
```typescript
// tests/pact/user-service.pact.test.ts
import { PactV3, MatchersV3 } from "@pact-foundation/pact";
import { UserService } from "../../src/services/UserService";
const { like, eachLike, iso8601DateTimeWithMillis } = MatchersV3;
const provider = new PactV3({
consumer: "OrderService",
provider: "UserService",
port: 1234,
dir: "./pacts",
});
describe("User Service Contract", () => {
const userService = new UserService("http://localhost:1234");
describe("GET /users/:id", () => {
test("returns user when found", async () => {
await provider
.given("user with ID 123 exists")
.uponReceiving("a request for user 123")
.withRequest({
method: "GET",
path: "/users/123",
headers: {
Authorization: like("Bearer token"),
},
})
.willRespondWith({
status: 200,
headers: {
"Content-Type": "application/json",
},
body: {
id: like("123"),
email: like("user@example.com"),
name: like("John Doe"),
age: like(30),
createdAt: iso8601DateTimeWithMillis("2024-01-01T00:00:00.000Z"),
role: like("user"),
},
})
.executeTest(async (mockServer) => {
const user = await userService.getUser("123");
expect(user.id).toBe("123");
expect(user.email).toBeDefined();
expect(user.name).toBeDefined();
});
});
test("returns 404 when user not found", async () => {
await provider
.given("user with ID 999 does not exist")
.uponReceiving("a request for non-existent user")
.withRequest({
method: "GET",
path: "/users/999",
})
.willRespondWith({
status: 404,
headers: {
"Content-Type": "application/json",
},
body: {
error: like("User not found"),
code: like("USER_NOT_FOUND"),
},
})
.executeTest(async (mockServer) => {
await expect(userService.getUser("999")).rejects.toThrow(
"User not found",
);
});
});
});
describe("POST /users", () => {
test("creates new user", async () => {
await provider
.given("user does not exist")
.uponReceiving("a request to create user")
.withRequest({
method: "POST",
path: "/users",
headers: {
"Content-Type": "application/json",
},
body: {
email: like("newuser@example.com"),
name: like("New User"),
age: like(25),
},
})
.willRespondWith({
status: 201,
headers: {
"Content-Type": "application/json",
},
body: {
id: like("new-123"),
email: like("newuser@example.com"),
name: like("New User"),
age: like(25),
createdAt: iso8601DateTimeWithMillis(),
role: "user",
},
})
.executeTest(async (mockServer) => {
const user = await userService.createUser({
email: "newuser@example.com",
name: "New User",
age: 25,
});
expect(user.id).toBeDefined();
expect(user.email).toBe("newuser@example.com");
});
});
});
describe("GET /users/:id/orders", () => {
test("returns user orders", async () => {
await provider
.given("user 123 has orders")
.uponReceiving("a request for user orders")
.withRequest({
method: "GET",
path: "/users/123/orders",
query: {
limit: "10",
offset: "0",
},
})
.willRespondWith({
status: 200,
body: {
orders: eachLike({
id: like("order-1"),
total: like(99.99),
status: like("completed"),
createdAt: iso8601DateTimeWithMillis(),
}),
total: like(5),
hasMore: like(false),
},
})
.executeTest(async (mockServer) => {
const response = await userService.getUserOrders("123", {
limit: 10,
offset: 0,
});
expect(response.orders).toBeDefined();
expect(Array.isArray(response.orders)).toBe(true);
expect(response.total).toBeDefined();
});
});
});
});
```
### Provider Test (Verify Contract)
```typescript
// tests/pact/user-service.provider.test.ts
import { Verifier } from "@pact-foundation/pact";
import path from "path";
import { app } from "../../src/app";
import { setupTestDB, teardownTestDB } from "../helpers/db";
describe("Pact Provider Verification", () => {
let server;
beforeAll(async () => {
await setupTestDB();
server = app.listen(3001);
});
afterAll(async () => {
await teardownTestDB();
server.close();
});
test("validates the expectations of OrderService", () => {
return new Verifier({
provider: "UserService",
providerBaseUrl: "http://localhost:3001",
pactUrls: [
path.resolve(__dirname, "../../pacts/orderservice-userservice.json"),
],
// Provider state setup
stateHandlers: {
"user with ID 123 exists": async () => {
await createTestUser({ id: "123", name: "John Doe" });
},
"user with ID 999 does not exist": async () => {
await deleteUser("999");
},
"user 123 has orders": async () => {
await createTestUser({ id: "123" });
await createTestOrder({ userId: "123" });
},
},
})
.verifyProvider()
.then((output) => {
console.log("Pact Verification Complete!");
});
});
});
```
references/rest-assured-for-java.md
# REST Assured for Java
## REST Assured for Java
```java
// ContractTest.java
import io.restassured.RestAssured;
import io.restassured.module.jsv.JsonSchemaValidator;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
public class UserAPIContractTest {
@Test
public void getUserShouldMatchSchema() {
given()
.pathParam("id", "123")
.when()
.get("/api/users/{id}")
.then()
.statusCode(200)
.body(JsonSchemaValidator.matchesJsonSchemaInClasspath("schemas/user-schema.json"))
.body("id", notNullValue())
.body("email", matchesPattern("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"))
.body("age", greaterThanOrEqualTo(0));
}
@Test
public void createUserShouldValidateRequest() {
String userJson = """
{
"email": "test@example.com",
"name": "Test User",
"age": 30
}
""";
given()
.contentType("application/json")
.body(userJson)
.when()
.post("/api/users")
.then()
.statusCode(201)
.body("id", notNullValue())
.body("email", equalTo("test@example.com"))
.body("createdAt", matchesPattern("\\d{4}-\\d{2}-\\d{2}T.*"));
}
@Test
public void getUserOrdersShouldReturnArray() {
given()
.pathParam("id", "123")
.queryParam("limit", 10)
.when()
.get("/api/users/{id}/orders")
.then()
.statusCode(200)
.body("orders", isA(java.util.List.class))
.body("orders[0].id", notNullValue())
.body("orders[0].status", isIn(Arrays.asList(
"pending", "paid", "shipped", "delivered", "cancelled"
)))
.body("total", greaterThanOrEqualTo(0));
}
@Test
public void invalidRequestShouldReturn400() {
String invalidUser = """
{
"email": "not-an-email",
"age": -5
}
""";
given()
.contentType("application/json")
.body(invalidUser)
.when()
.post("/api/users")
.then()
.statusCode(400)
.body("error", notNullValue());
}
}
```
scripts/scaffold-tests.sh
#!/bin/bash
# scaffold-tests.sh - Generate test file scaffolding
# Usage: ./scaffold-tests.sh <source_file> [--framework jest|pytest|mocha]
set -euo pipefail
SOURCE_FILE="${{1:?Usage: $0 <source_file> [--framework jest|pytest|mocha]}}"
FRAMEWORK="${{2:-jest}}"
echo "Scaffolding tests for: $SOURCE_FILE (framework: $FRAMEWORK)"
# TODO: Implement test scaffolding logic
# - Parse source file for exported functions/classes
# - Generate test stubs for each export
# - Include setup/teardown boilerplate
# - Add common assertion patterns
echo "Test scaffolding complete."
templates/test-template.js
// Test Template
// TODO: Customize for your testing framework and project
describe('ModuleName', () => {
// Setup
beforeEach(() => {
// TODO: Add test setup
});
afterEach(() => {
// TODO: Add cleanup
});
describe('functionName', () => {
it('should handle the happy path', () => {
// TODO: Add assertion
});
it('should handle edge cases', () => {
// TODO: Add edge case tests
});
it('should handle errors gracefully', () => {
// TODO: Add error handling tests
});
});
});