Agent skill
testing
API testing strategies and contract testing
Install this agent skill to your Project
npx add-skill https://github.com/pluginagentmarketplace/custom-plugin-api-design/tree/main/skills/testing
SKILL.md
API Testing Skill
Purpose
Implement comprehensive API testing strategies.
Testing Pyramid
╱╲
╱E2E╲ Few, slow, expensive
╱──────╲
╱ Contract╲ Consumer-driven contracts
╱────────────╲
╱ Integration ╲ API + Database
╱────────────────╲
╱ Unit Tests ╲ Fast, many, cheap
╱────────────────────╲
Unit Testing
Controller/Handler Tests
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UserController } from './user.controller';
import { UserService } from './user.service';
describe('UserController', () => {
let controller: UserController;
let mockUserService: jest.Mocked<UserService>;
beforeEach(() => {
mockUserService = {
findById: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
};
controller = new UserController(mockUserService);
});
describe('getUser', () => {
it('should return user when found', async () => {
const mockUser = { id: '123', name: 'John', email: 'john@test.com' };
mockUserService.findById.mockResolvedValue(mockUser);
const result = await controller.getUser('123');
expect(result).toEqual({ data: mockUser });
expect(mockUserService.findById).toHaveBeenCalledWith('123');
});
it('should throw NotFoundException when user not found', async () => {
mockUserService.findById.mockResolvedValue(null);
await expect(controller.getUser('999')).rejects.toThrow('User not found');
});
});
describe('createUser', () => {
it('should create and return new user', async () => {
const input = { email: 'new@test.com', name: 'New User' };
const created = { id: '456', ...input };
mockUserService.create.mockResolvedValue(created);
const result = await controller.createUser(input);
expect(result.data.id).toBe('456');
});
it('should validate input', async () => {
const invalidInput = { email: 'invalid', name: '' };
await expect(controller.createUser(invalidInput)).rejects.toThrow('Validation');
});
});
});
Service/Business Logic Tests
describe('UserService', () => {
let service: UserService;
let mockRepository: jest.Mocked<UserRepository>;
beforeEach(() => {
mockRepository = {
findById: vi.fn(),
save: vi.fn(),
delete: vi.fn(),
};
service = new UserService(mockRepository);
});
describe('create', () => {
it('should hash password before saving', async () => {
const input = { email: 'test@test.com', name: 'Test', password: 'secret123' };
await service.create(input);
expect(mockRepository.save).toHaveBeenCalledWith(
expect.objectContaining({
email: 'test@test.com',
passwordHash: expect.not.stringContaining('secret123'),
})
);
});
it('should throw ConflictError for duplicate email', async () => {
mockRepository.save.mockRejectedValue({ code: '23505' }); // Unique violation
await expect(service.create({ email: 'existing@test.com' }))
.rejects.toThrow('Email already exists');
});
});
});
Integration Testing
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import request from 'supertest';
import { app } from './app';
import { db } from './database';
describe('Users API Integration', () => {
beforeAll(async () => {
await db.migrate.latest();
await db.seed.run();
});
afterAll(async () => {
await db.destroy();
});
describe('GET /api/v1/users', () => {
it('should return paginated list', async () => {
const res = await request(app)
.get('/api/v1/users?page=1&limit=10')
.set('Authorization', `Bearer ${testToken}`)
.expect(200);
expect(res.body.data).toBeInstanceOf(Array);
expect(res.body.pagination).toMatchObject({
page: 1,
limit: 10,
total: expect.any(Number),
});
});
});
describe('POST /api/v1/users', () => {
it('should persist user to database', async () => {
const userData = {
email: 'integration@test.com',
name: 'Integration Test',
password: 'SecurePass123!',
};
const res = await request(app)
.post('/api/v1/users')
.set('Authorization', `Bearer ${adminToken}`)
.send(userData)
.expect(201);
// Verify in database
const dbUser = await db('users')
.where('id', res.body.data.id)
.first();
expect(dbUser).toBeDefined();
expect(dbUser.email).toBe(userData.email);
});
});
});
Contract Testing (Pact)
Consumer Side
import { Pact } from '@pact-foundation/pact';
import { UserApiClient } from './user-api-client';
describe('User API Contract', () => {
const provider = new Pact({
consumer: 'WebApp',
provider: 'UserService',
port: 1234,
});
beforeAll(() => provider.setup());
afterAll(() => provider.finalize());
afterEach(() => provider.verify());
describe('get user by id', () => {
it('should return user when exists', async () => {
await provider.addInteraction({
state: 'user with id 123 exists',
uponReceiving: 'a request for user 123',
withRequest: {
method: 'GET',
path: '/api/v1/users/123',
headers: { Accept: 'application/json' },
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
data: {
id: '123',
name: like('John Doe'),
email: like('john@example.com'),
},
},
},
});
const client = new UserApiClient('http://localhost:1234');
const user = await client.getUser('123');
expect(user.id).toBe('123');
});
});
});
Provider Side
import { Verifier } from '@pact-foundation/pact';
describe('Pact Verification', () => {
it('should validate consumer contracts', async () => {
const verifier = new Verifier({
providerBaseUrl: 'http://localhost:3000',
pactBrokerUrl: process.env.PACT_BROKER_URL,
provider: 'UserService',
publishVerificationResult: true,
stateHandlers: {
'user with id 123 exists': async () => {
await db('users').insert({ id: '123', name: 'John', email: 'john@test.com' });
},
},
});
await verifier.verifyProvider();
});
});
Load Testing (k6)
// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '1m', target: 50 }, // Ramp up
{ duration: '3m', target: 50 }, // Steady state
{ duration: '1m', target: 100 }, // Peak load
{ duration: '1m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% under 500ms
http_req_failed: ['rate<0.01'], // <1% error rate
},
};
export default function () {
const token = __ENV.API_TOKEN;
// List users
const listRes = http.get('http://api.example.com/api/v1/users?limit=20', {
headers: { Authorization: `Bearer ${token}` },
});
check(listRes, {
'list status 200': (r) => r.status === 200,
'list has data': (r) => JSON.parse(r.body).data.length > 0,
});
sleep(1);
// Get single user
const getRes = http.get('http://api.example.com/api/v1/users/123', {
headers: { Authorization: `Bearer ${token}` },
});
check(getRes, {
'get status 200': (r) => r.status === 200,
});
sleep(1);
}
Run: k6 run --env API_TOKEN=xxx load-test.js
Security Testing
describe('Security Tests', () => {
describe('Authentication', () => {
it('should reject requests without token', async () => {
await request(app)
.get('/api/v1/users')
.expect(401);
});
it('should reject expired tokens', async () => {
await request(app)
.get('/api/v1/users')
.set('Authorization', `Bearer ${expiredToken}`)
.expect(401);
});
});
describe('Authorization', () => {
it('should prevent accessing other users data', async () => {
await request(app)
.get('/api/v1/users/other-user-id')
.set('Authorization', `Bearer ${userToken}`)
.expect(403);
});
});
describe('Input Validation', () => {
it('should reject SQL injection attempts', async () => {
await request(app)
.get(`/api/v1/users?search='; DROP TABLE users; --`)
.set('Authorization', `Bearer ${token}`)
.expect(400);
});
it('should sanitize XSS in input', async () => {
const res = await request(app)
.post('/api/v1/users')
.set('Authorization', `Bearer ${adminToken}`)
.send({ name: '<script>alert("xss")</script>' })
.expect(201);
expect(res.body.data.name).not.toContain('<script>');
});
});
describe('Rate Limiting', () => {
it('should block after exceeding limit', async () => {
// Make many requests
for (let i = 0; i < 100; i++) {
await request(app).get('/api/v1/users');
}
// Should be rate limited
await request(app)
.get('/api/v1/users')
.expect(429);
});
});
});
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| Flaky tests | Shared state | Isolate test data |
| Slow integration tests | Database setup | Use transactions |
| Contract mismatches | Schema changes | Version contracts |
| Load test failures | Connection limits | Check pool size |
Quality Checklist
- Unit test coverage > 80%
- Integration tests for critical paths
- Contract tests with consumers
- Load tests before releases
- Security tests automated
- Tests run in CI/CD
- Test data isolated
- Mock external services
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
versioning
API versioning strategies and backward compatibility
frontend-patterns
Frontend development and API integration patterns for React, TypeScript, and state management
rest
RESTful API design principles and best practices
graphql
GraphQL API design and schema development
documentation
API documentation with OpenAPI and developer portals
database-patterns
Database design, optimization, and caching strategies for SQL, NoSQL, and Redis
Didn't find tool you were looking for?