Testing Your API
Comprehensive guide to testing DataBridge APIs with automated tests and best practices
Testing Your API
Learn how to effectively test your DataBridge-generated API.
Overview
DataBridge automatically generates test scripts, but you should also write custom tests for:
- Business logic
- Edge cases
- Integration scenarios
- Performance
Auto-Generated Tests
When you run databridge generate, you get test-api.sh with tests for:
- Health checks
- CRUD operations
- Validation rules
- Error handling
Running Generated Tests
# Start your API
npm run dev
# In another terminal
chmod +x test-api.sh
./test-api.sh
Example Output
=========================================
DataBridge API Test Suite
=========================================
Test: Health Check
✅ Pass
Test: GET /products (List all)
✅ Pass
Test: POST /products (Create valid)
✅ Pass
Test: POST /products (Missing required field)
✅ Pass - Validation caught missing field
Test: POST /products (Negative price)
✅ Pass - Validation caught negative price
=========================================
Test Suite Complete: 5/5 passed
=========================================
Manual Testing with cURL
GET Requests
# List all products
curl http://localhost:3000/products
# Get single product
curl http://localhost:3000/products/1
# With query parameters
curl "http://localhost:3000/products?category=electronics&sort=price"
POST Requests
# Create product
curl -X POST http://localhost:3000/products \
-H "Content-Type: application/json" \
-d '{
"name": "Laptop Pro",
"description": "High-performance laptop",
"price": 1299.99,
"stock": 10,
"category": "electronics"
}'
# Test validation
curl -X POST http://localhost:3000/products \
-H "Content-Type: application/json" \
-d '{"name": "", "price": -10}'
PATCH Requests
# Update product
curl -X PATCH http://localhost:3000/products/1 \
-H "Content-Type: application/json" \
-d '{"stock": 15, "price": 1199.99}'
DELETE Requests
# Delete product
curl -X DELETE http://localhost:3000/products/1 \
-w "\nHTTP Status: %{http_code}\n"
Unit Testing with Vitest
Setup
npm install -D vitest @vitest/ui
vitest.config.ts:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
},
},
});
Testing Validation Schemas
tests/validation.test.ts:
import { describe, it, expect } from 'vitest';
import { z } from 'zod';
const productSchema = z.object({
name: z.string().min(1).max(255),
price: z.number().positive(),
stock: z.number().int().nonnegative().optional(),
});
describe('Product Validation', () => {
it('accepts valid product', () => {
const valid = {
name: 'Laptop',
price: 999.99,
stock: 10,
};
const result = productSchema.safeParse(valid);
expect(result.success).toBe(true);
});
it('rejects empty name', () => {
const invalid = {
name: '',
price: 999.99,
};
const result = productSchema.safeParse(invalid);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.errors[0].path).toEqual(['name']);
}
});
it('rejects negative price', () => {
const invalid = {
name: 'Laptop',
price: -10,
};
const result = productSchema.safeParse(invalid);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.errors[0].path).toEqual(['price']);
}
});
it('rejects string for stock', () => {
const invalid = {
name: 'Laptop',
price: 999.99,
stock: 'ten' as any,
};
const result = productSchema.safeParse(invalid);
expect(result.success).toBe(false);
});
it('accepts optional stock', () => {
const valid = {
name: 'Laptop',
price: 999.99,
};
const result = productSchema.safeParse(valid);
expect(result.success).toBe(true);
});
});
Run tests:
npx vitest
npx vitest --ui # Open UI
npx vitest --coverage # Generate coverage
Integration Testing
Test entire API routes with real HTTP requests.
Setup
npm install -D supertest @types/supertest
Testing Routes
tests/products.integration.test.ts:
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import Fastify, { FastifyInstance } from 'fastify';
import productsRoutes from '../src/routes/products';
import prismaPlugin from '../src/plugins/prisma';
describe('Products API', () => {
let fastify: FastifyInstance;
beforeAll(async () => {
fastify = Fastify();
await fastify.register(prismaPlugin);
await fastify.register(productsRoutes);
await fastify.ready();
});
afterAll(async () => {
await fastify.close();
});
describe('GET /products', () => {
it('returns array of products', async () => {
const response = await fastify.inject({
method: 'GET',
url: '/products',
});
expect(response.statusCode).toBe(200);
expect(Array.isArray(JSON.parse(response.body))).toBe(true);
});
});
describe('POST /products', () => {
it('creates product with valid data', async () => {
const response = await fastify.inject({
method: 'POST',
url: '/products',
payload: {
name: 'Test Product',
price: 99.99,
stock: 10,
},
});
expect(response.statusCode).toBe(201);
const product = JSON.parse(response.body);
expect(product).toHaveProperty('id');
expect(product.name).toBe('Test Product');
expect(product.price).toBe('99.99');
});
it('rejects invalid data', async () => {
const response = await fastify.inject({
method: 'POST',
url: '/products',
payload: {
name: '',
price: -10,
},
});
expect(response.statusCode).toBe(400);
const error = JSON.parse(response.body);
expect(error.error).toBe('Validation failed');
expect(error.issues).toBeDefined();
});
it('rejects missing required fields', async () => {
const response = await fastify.inject({
method: 'POST',
url: '/products',
payload: {},
});
expect(response.statusCode).toBe(400);
});
});
describe('GET /products/:id', () => {
it('returns product by id', async () => {
// First create a product
const createResponse = await fastify.inject({
method: 'POST',
url: '/products',
payload: {
name: 'Find Me',
price: 49.99,
},
});
const created = JSON.parse(createResponse.body);
// Then fetch it
const response = await fastify.inject({
method: 'GET',
url: `/products/${created.id}`,
});
expect(response.statusCode).toBe(200);
const product = JSON.parse(response.body);
expect(product.id).toBe(created.id);
expect(product.name).toBe('Find Me');
});
it('returns 404 for non-existent product', async () => {
const response = await fastify.inject({
method: 'GET',
url: '/products/99999',
});
expect(response.statusCode).toBe(404);
});
it('returns 400 for invalid id', async () => {
const response = await fastify.inject({
method: 'GET',
url: '/products/abc',
});
expect(response.statusCode).toBe(400);
});
});
describe('PATCH /products/:id', () => {
it('updates product', async () => {
// Create product
const createResponse = await fastify.inject({
method: 'POST',
url: '/products',
payload: {
name: 'Original Name',
price: 100,
},
});
const created = JSON.parse(createResponse.body);
// Update it
const response = await fastify.inject({
method: 'PATCH',
url: `/products/${created.id}`,
payload: {
name: 'Updated Name',
},
});
expect(response.statusCode).toBe(200);
const updated = JSON.parse(response.body);
expect(updated.name).toBe('Updated Name');
expect(updated.price).toBe(created.price); // Unchanged
});
});
describe('DELETE /products/:id', () => {
it('deletes product', async () => {
// Create product
const createResponse = await fastify.inject({
method: 'POST',
url: '/products',
payload: {
name: 'Delete Me',
price: 10,
},
});
const created = JSON.parse(createResponse.body);
// Delete it
const response = await fastify.inject({
method: 'DELETE',
url: `/products/${created.id}`,
});
expect(response.statusCode).toBe(204);
// Verify it's gone
const getResponse = await fastify.inject({
method: 'GET',
url: `/products/${created.id}`,
});
expect(getResponse.statusCode).toBe(404);
});
});
});
E2E Testing with Playwright
Test your API through the browser (for full stack apps).
Setup
npm install -D @playwright/test
npx playwright install
playwright.config.ts:
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
use: {
baseURL: 'http://localhost:3000',
},
webServer: {
command: 'npm run dev',
port: 3000,
reuseExistingServer: !process.env.CI,
},
});
E2E Test Example
tests/e2e/products.spec.ts:
import { test, expect } from '@playwright/test';
test.describe('Products API E2E', () => {
test('can create and fetch product', async ({ request }) => {
// Create product
const createResponse = await request.post('/products', {
data: {
name: 'E2E Test Product',
price: 149.99,
stock: 25,
},
});
expect(createResponse.ok()).toBeTruthy();
const created = await createResponse.json();
expect(created).toHaveProperty('id');
// Fetch product
const getResponse = await request.get(`/products/${created.id}`);
expect(getResponse.ok()).toBeTruthy();
const fetched = await getResponse.json();
expect(fetched.name).toBe('E2E Test Product');
});
test('validation errors return 400', async ({ request }) => {
const response = await request.post('/products', {
data: {
name: '',
price: -10,
},
});
expect(response.status()).toBe(400);
const error = await response.json();
expect(error.error).toBe('Validation failed');
});
});
Run tests:
npx playwright test
npx playwright test --ui
npx playwright show-report
Load Testing
Test API performance under load.
Using Apache Bench (ab)
# 1000 requests, 10 concurrent
ab -n 1000 -c 10 http://localhost:3000/products
Using autocannon
npm install -g autocannon
# Run load test
autocannon -c 100 -d 30 http://localhost:3000/products
# Output:
# ┌─────────┬────────┬─────────┬─────────┬─────────┬─────────┐
# │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │
# ├─────────┼────────┼─────────┼─────────┼─────────┼─────────┤
# │ Latency │ 10 ms │ 15 ms │ 45 ms │ 60 ms │ 18 ms │
# └─────────┴────────┴─────────┴─────────┴─────────┴─────────┘
# 100k requests in 30s, 15 MB read
Custom Load Test Script
tests/load-test.ts:
import autocannon from 'autocannon';
async function runLoadTest() {
const result = await autocannon({
url: 'http://localhost:3000',
connections: 100,
duration: 30,
requests: [
{
method: 'GET',
path: '/products',
},
{
method: 'GET',
path: '/health',
},
],
});
console.log(result);
}
runLoadTest();
Database Testing
Test Database Setup
Use separate test database:
.env.test:
DATABASE_URL="mysql://root:password@localhost:3306/mydb_test"
Seeding Test Data
prisma/seed.ts:
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
// Clear existing data
await prisma.products.deleteMany();
await prisma.users.deleteMany();
// Seed test data
await prisma.users.create({
data: {
email: 'test@example.com',
name: 'Test User',
},
});
await prisma.products.createMany({
data: [
{
name: 'Test Product 1',
price: 99.99,
stock: 10,
},
{
name: 'Test Product 2',
price: 149.99,
stock: 5,
},
],
});
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
Run seed:
npx prisma db seed
CI/CD Integration
GitHub Actions
.github/workflows/test.yml:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: test
MYSQL_DATABASE: mydb_test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Generate Prisma client
run: npx prisma generate
- name: Run migrations
run: npx prisma migrate deploy
env:
DATABASE_URL: mysql://root:test@127.0.0.1:3306/mydb_test
- name: Run tests
run: npm test
env:
DATABASE_URL: mysql://root:test@127.0.0.1:3306/mydb_test
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json
Best Practices
DO ✅
- Write tests before fixing bugs (TDD)
- Test edge cases and error conditions
- Use separate test database
- Clean up test data after tests
- Mock external services
- Test validation thoroughly
- Test authentication/authorization
- Monitor test performance
- Run tests in CI/CD
- Maintain > 80% coverage
DON’T ❌
- Test against production database
- Skip error case testing
- Hard-code test data IDs
- Leave test data in database
- Test only happy paths
- Ignore flaky tests
- Skip integration tests
- Test implementation details
Test Coverage
Check coverage:
npx vitest --coverage
Example output:
% Coverage report from v8
----------------------------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
----------------------------|---------|----------|---------|---------|
All files | 92.5 | 87.3 | 95.0 | 92.8 |
routes/products.ts | 95.2 | 90.0 | 100 | 95.4 |
routes/users.ts | 93.1 | 88.0 | 100 | 93.5 |
plugins/prisma.ts | 85.0 | 75.0 | 80.0 | 85.2 |
----------------------------|---------|----------|---------|---------|
Additional Resources
Was this page helpful?
Thank you for your feedback!