Backend Testing - 2/2
From Unit Testing to Production Confidence
You’ve mastered testing fundamentals with strategic test pyramids that balance speed and coverage, comprehensive unit tests that validate business logic and edge cases, integration tests that ensure components work together reliably, and sophisticated mocking strategies that enable testing without external dependencies. Your test suites catch bugs early and prevent regressions through isolated, fast-running validation. But here’s the production reality that separates tested code from bulletproof systems: even with perfect unit tests, flawless integration tests, and comprehensive mocks, your application is vulnerable if it can’t handle real user loads, API contract changes, or the chaotic conditions of production environments.
The production testing gap that destroys user experience:
// Your perfectly unit-tested API that crumbles under real-world conditions
app.post(
"/api/orders",
validateOrderInput,
authenticateUser,
async (req, res) => {
try {
// Unit tests pass: ✅ Business logic works
// Integration tests pass: ✅ Database operations work
// Mocks work perfectly: ✅ External services mocked
const order = await processOrder(req.body);
const payment = await processPayment(order.total);
const shipment = await scheduleShipment(order.items);
res.json({ success: true, orderId: order.id });
// Hidden problems that only show up in production:
// - API response time: 8 seconds under normal load (users abandon)
// - Memory leak: 50MB per order processing (server crashes after 100 orders)
// - Database connection pool exhaustion after 20 concurrent requests
// - Payment API rate limits cause 429 errors during peak hours
// - Shipping API contract changed: new required fields break integration
// - Error responses don't match API documentation
// - CORS headers missing for frontend integration
// - Request size limits cause 413 errors for large orders
} catch (error) {
res.status(500).json({ error: "Order processing failed" });
}
}
);
// The production reality check that unit tests miss:
// - "It works on my machine" but fails in staging
// - Performance degrades exponentially under load
// - APIs break when external services update
// - Error scenarios that only happen with real user patterns
// - Integration failures that mocks can't simulate
The uncomfortable production truth: Comprehensive unit and integration tests mean nothing if your APIs can’t handle real user loads, if performance degrades under pressure, or if external service changes break your integrations. Modern applications require testing strategies that validate not just correctness, but performance, resilience, and real-world integration scenarios.
Real-world production testing failure consequences:
// What happens when production testing is incomplete:
const productionTestingFailureImpact = {
performanceCollapse: {
problem:
"API response times increase from 200ms to 8+ seconds during flash sale",
cause: "No load testing revealed database query N+1 problem",
impact: "87% of users abandon checkout, $1.3M in lost sales",
discovery: "Customers complained on social media during peak traffic",
solution: "Emergency database query optimization and connection pooling",
},
integrationBreakage: {
problem: "Payment processing fails after Stripe API v2023-10 update",
cause: "API contract tests not automated, breaking changes undetected",
impact: "4 hours of failed payments, 2,847 angry customers",
emergency: "Manual rollback to previous API version",
reputation: "Payment reliability concerns lead to 18% customer churn",
},
cascadingFailure: {
problem: "Recommendation service timeout brings down entire product page",
cause: "No circuit breaker testing, timeouts not properly configured",
impact: "Complete site outage for 2.5 hours during prime shopping hours",
scope: "Affected all product pages, search, and checkout flow",
cost: "$420K in lost revenue + emergency incident response",
},
// Perfect unit tests are meaningless when real-world conditions
// reveal performance bottlenecks and integration vulnerabilities
};
Advanced backend testing mastery requires understanding:
- API testing (automated and manual) that validates contracts, error scenarios, and real-world integration patterns
- Load testing and performance testing that reveals bottlenecks before users encounter them
- Test-driven development (TDD) that designs better code through testing-first approaches
- Continuous testing practices that integrate quality gates into deployment pipelines
- Testing environments and data that mirror production conditions accurately
This article completes your testing education by building production-ready testing strategies. You’ll create API test suites that catch breaking changes, performance tests that reveal bottlenecks, TDD workflows that improve code design, and continuous testing pipelines that prevent regressions from reaching users.
API Testing: Contract Validation and Integration Assurance
Comprehensive API Testing Strategy
The API testing approach that catches integration issues early:
// ✅ Professional API testing with contract validation and error scenario coverage
// tests/api/UserAPI.test.js
describe("User API Integration Tests", () => {
let app;
let server;
let testDb;
beforeAll(async () => {
// Setup test environment
testDb = await createTestDatabase();
app = createApp({ database: testDb, environment: "test" });
server = app.listen(0); // Random available port
});
afterAll(async () => {
await server.close();
await testDb.cleanup();
});
beforeEach(async () => {
// Clean test data
await testDb.users.deleteMany({});
await testDb.sessions.deleteMany({});
});
describe("POST /api/users/register", () => {
const validUserData = {
email: "test@example.com",
password: "SecurePass123!",
firstName: "John",
lastName: "Doe",
};
describe("Success Scenarios", () => {
it("creates user with valid data and returns expected structure", async () => {
// Act
const response = await request(app)
.post("/api/users/register")
.send(validUserData)
.expect("Content-Type", /json/)
.expect(201);
// Assert response structure matches API contract
expect(response.body).toMatchSchema({
type: "object",
required: ["success", "message", "userId", "user"],
properties: {
success: { type: "boolean", enum: [true] },
message: { type: "string" },
userId: { type: "string", format: "uuid" },
user: {
type: "object",
required: ["id", "email", "firstName", "lastName", "createdAt"],
properties: {
id: { type: "string" },
email: { type: "string", format: "email" },
firstName: { type: "string" },
lastName: { type: "string" },
createdAt: { type: "string", format: "date-time" },
// Password should never be in response
password: { not: {} },
},
},
},
});
// Verify actual user creation
const createdUser = await testDb.users.findOne({
email: validUserData.email,
});
expect(createdUser).toBeDefined();
expect(createdUser.emailVerified).toBe(false);
});
it("handles international characters in names correctly", async () => {
const internationalUserData = {
...validUserData,
firstName: "José",
lastName: "Müller-García",
email: "josé.müller@example.com",
};
const response = await request(app)
.post("/api/users/register")
.send(internationalUserData)
.expect(201);
expect(response.body.user.firstName).toBe("José");
expect(response.body.user.lastName).toBe("Müller-García");
// Verify database stores UTF-8 correctly
const dbUser = await testDb.users.findOne({
email: internationalUserData.email,
});
expect(dbUser.firstName).toBe("José");
});
});
describe("Validation Error Scenarios", () => {
it("returns 400 for missing required fields", async () => {
const incompleteData = { email: "test@example.com" };
const response = await request(app)
.post("/api/users/register")
.send(incompleteData)
.expect("Content-Type", /json/)
.expect(400);
expect(response.body).toMatchSchema({
type: "object",
required: ["error", "message", "validationErrors"],
properties: {
error: { type: "string" },
message: { type: "string" },
validationErrors: {
type: "array",
items: {
type: "object",
required: ["field", "message"],
properties: {
field: { type: "string" },
message: { type: "string" },
},
},
},
},
});
// Verify specific validation errors
const errors = response.body.validationErrors;
const missingFields = errors.map((e) => e.field);
expect(missingFields).toContain("password");
expect(missingFields).toContain("firstName");
expect(missingFields).toContain("lastName");
});
it("returns 400 for invalid email format", async () => {
const invalidEmailData = {
...validUserData,
email: "not-an-email",
};
const response = await request(app)
.post("/api/users/register")
.send(invalidEmailData)
.expect(400);
const emailError = response.body.validationErrors.find(
(e) => e.field === "email"
);
expect(emailError.message).toMatch(/valid email/i);
});
it("returns 400 for weak password", async () => {
const weakPasswordData = {
...validUserData,
password: "123",
};
const response = await request(app)
.post("/api/users/register")
.send(weakPasswordData)
.expect(400);
const passwordError = response.body.validationErrors.find(
(e) => e.field === "password"
);
expect(passwordError.message).toMatch(/password requirements/i);
});
});
describe("Conflict Scenarios", () => {
it("returns 409 for duplicate email registration", async () => {
// Create existing user
await testDb.users.insertOne({
id: "existing123",
email: validUserData.email,
hashedPassword: "hashed123",
});
const response = await request(app)
.post("/api/users/register")
.send(validUserData)
.expect("Content-Type", /json/)
.expect(409);
expect(response.body).toMatchObject({
error: "Email already registered",
message: expect.stringMatching(/account.*already exists/i),
});
});
});
describe("Rate Limiting", () => {
it("enforces rate limits for registration attempts", async () => {
const rateLimitRequests = [];
// Send multiple requests rapidly
for (let i = 0; i < 10; i++) {
const userData = {
...validUserData,
email: `test${i}@example.com`,
};
rateLimitRequests.push(
request(app).post("/api/users/register").send(userData)
);
}
const responses = await Promise.all(rateLimitRequests);
// Some requests should be rate limited
const rateLimitedResponses = responses.filter((r) => r.status === 429);
expect(rateLimitedResponses.length).toBeGreaterThan(0);
// Rate limit response should include retry headers
const rateLimitResponse = rateLimitedResponses[0];
expect(rateLimitResponse.headers["retry-after"]).toBeDefined();
expect(rateLimitResponse.body.error).toMatch(/rate limit/i);
});
});
});
describe("GET /api/users/profile", () => {
let authenticatedUser;
let authToken;
beforeEach(async () => {
// Create authenticated user for protected endpoint testing
authenticatedUser = await testDb.users.insertOne({
id: "user123",
email: "auth@example.com",
firstName: "Auth",
lastName: "User",
emailVerified: true,
});
// Create auth token/session
const response = await request(app).post("/api/auth/login").send({
email: "auth@example.com",
password: "TestPassword123!",
});
authToken = response.body.token;
});
describe("Authentication Tests", () => {
it("returns user profile for authenticated request", async () => {
const response = await request(app)
.get("/api/users/profile")
.set("Authorization", `Bearer ${authToken}`)
.expect("Content-Type", /json/)
.expect(200);
expect(response.body).toMatchSchema({
type: "object",
required: ["user"],
properties: {
user: {
type: "object",
required: ["id", "email", "firstName", "lastName"],
properties: {
id: { type: "string" },
email: { type: "string" },
firstName: { type: "string" },
lastName: { type: "string" },
// Sensitive fields should not be included
password: { not: {} },
hashedPassword: { not: {} },
},
},
},
});
});
it("returns 401 for missing authentication", async () => {
const response = await request(app)
.get("/api/users/profile")
.expect("Content-Type", /json/)
.expect(401);
expect(response.body).toMatchObject({
error: "Authentication required",
message: expect.any(String),
});
});
it("returns 401 for invalid token", async () => {
const response = await request(app)
.get("/api/users/profile")
.set("Authorization", "Bearer invalid-token")
.expect(401);
expect(response.body.error).toMatch(/invalid.*token/i);
});
it("returns 401 for expired token", async () => {
// Create expired token
const expiredToken = jwt.sign(
{ userId: "user123" },
process.env.JWT_SECRET,
{ expiresIn: "-1h" }
);
const response = await request(app)
.get("/api/users/profile")
.set("Authorization", `Bearer ${expiredToken}`)
.expect(401);
expect(response.body.error).toMatch(/expired/i);
});
});
});
describe("API Contract Compliance", () => {
it("includes required CORS headers", async () => {
const response = await request(app)
.options("/api/users/register")
.set("Origin", "https://frontend.example.com")
.set("Access-Control-Request-Method", "POST")
.expect(200);
expect(response.headers["access-control-allow-origin"]).toBe("*");
expect(response.headers["access-control-allow-methods"]).toContain(
"POST"
);
expect(response.headers["access-control-allow-headers"]).toContain(
"Content-Type"
);
});
it("includes security headers", async () => {
const response = await request(app).get("/api/health").expect(200);
expect(response.headers["x-content-type-options"]).toBe("nosniff");
expect(response.headers["x-frame-options"]).toBe("DENY");
expect(response.headers["x-xss-protection"]).toBe("1; mode=block");
});
it("handles request size limits appropriately", async () => {
const oversizedData = {
email: "test@example.com",
password: "SecurePass123!",
firstName: "x".repeat(10000), // Oversized field
lastName: "Doe",
};
const response = await request(app)
.post("/api/users/register")
.send(oversizedData)
.expect(413); // Payload too large
expect(response.body.error).toMatch(/payload.*large|request.*size/i);
});
it("returns consistent error format across all endpoints", async () => {
// Test multiple error scenarios to ensure consistent format
const errorResponses = await Promise.all([
request(app).post("/api/users/register").send({}), // Validation error
request(app).get("/api/users/profile"), // Auth error
request(app).get("/api/nonexistent"), // Not found error
]);
// All error responses should have consistent structure
errorResponses.forEach((response) => {
expect(response.body).toHaveProperty("error");
expect(response.body).toHaveProperty("message");
expect(typeof response.body.error).toBe("string");
expect(typeof response.body.message).toBe("string");
});
});
});
});
// Contract testing with external API dependencies
describe("External API Contract Tests", () => {
describe("Payment Service Integration", () => {
let mockPaymentServer;
beforeAll(async () => {
// Setup mock payment service that mimics real API
mockPaymentServer = await createMockPaymentAPI();
});
afterAll(async () => {
await mockPaymentServer.close();
});
it("handles payment service API changes gracefully", async () => {
// Simulate API contract change
mockPaymentServer.updateAPIVersion("2023-10-16");
const paymentRequest = {
amount: 2000,
currency: "usd",
source: "tok_visa",
description: "Test payment",
};
try {
const response = await paymentService.processPayment(paymentRequest);
// Verify response matches expected contract
expect(response).toMatchSchema(PAYMENT_RESPONSE_SCHEMA);
} catch (error) {
// If API change breaks our integration, test should document the issue
expect(error.message).toMatch(/payment api/i);
console.warn("Payment API contract change detected:", error.message);
}
});
it("handles payment service timeouts appropriately", async () => {
// Configure mock to simulate slow response
mockPaymentServer.setResponseDelay(5000);
const paymentRequest = {
amount: 1000,
currency: "usd",
source: "tok_visa",
};
// Should timeout and handle gracefully
await expect(
paymentService.processPayment(paymentRequest)
).rejects.toThrow(/timeout/i);
// Verify proper timeout handling
const timeoutStart = Date.now();
try {
await paymentService.processPayment(paymentRequest);
} catch (error) {
const timeoutDuration = Date.now() - timeoutStart;
expect(timeoutDuration).toBeLessThan(4000); // Should timeout before 4s
}
});
});
});
Load Testing and Performance Testing: Scalability Validation
Professional Performance Testing Strategy
Load testing that reveals bottlenecks before users encounter them:
// ✅ Comprehensive performance testing with k6 and custom metrics
// performance/load-tests/api-load-test.js
import http from "k6/http";
import { check, sleep } from "k6";
import { Rate, Counter, Trend } from "k6/metrics";
// Custom metrics for detailed performance tracking
const errorRate = new Rate("error_rate");
const apiCalls = new Counter("api_calls_total");
const responseTimeP95 = new Trend("response_time_p95");
const databaseConnections = new Trend("database_connections");
const memoryUsage = new Trend("memory_usage_mb");
// Load testing configuration
export let options = {
stages: [
{ duration: "2m", target: 10 }, // Ramp up to 10 users over 2 minutes
{ duration: "5m", target: 10 }, // Stay at 10 users for 5 minutes
{ duration: "2m", target: 50 }, // Ramp up to 50 users over 2 minutes
{ duration: "5m", target: 50 }, // Stay at 50 users for 5 minutes
{ duration: "2m", target: 100 }, // Ramp up to 100 users over 2 minutes
{ duration: "10m", target: 100 }, // Stay at 100 users for 10 minutes
{ duration: "5m", target: 200 }, // Spike to 200 users
{ duration: "2m", target: 0 }, // Ramp down to 0 users
],
thresholds: {
// Performance requirements
http_req_duration: ["p(95)<500"], // 95% of requests must complete under 500ms
http_req_failed: ["rate<0.01"], // Error rate must be less than 1%
// Custom thresholds
error_rate: ["rate<0.01"],
response_time_p95: ["p(95)<500"],
// Infrastructure thresholds
database_connections: ["p(95)<80"], // DB connection pool limit
memory_usage_mb: ["p(95)<512"], // Memory usage limit
},
// Test environment configuration
ext: {
loadimpact: {
projectID: parseInt(__ENV.K6_PROJECT_ID || "0"),
name: "API Load Test",
},
},
};
// Test data setup
const BASE_URL = __ENV.BASE_URL || "http://localhost:3000";
const API_KEY = __ENV.API_KEY || "test-api-key";
// User authentication pool for realistic testing
const userPool = [
{ email: "user1@example.com", password: "TestPass123!" },
{ email: "user2@example.com", password: "TestPass123!" },
{ email: "user3@example.com", password: "TestPass123!" },
// ... more test users
];
// Authentication helper
function authenticate(user) {
const loginResponse = http.post(
`${BASE_URL}/api/auth/login`,
JSON.stringify({
email: user.email,
password: user.password,
}),
{
headers: {
"Content-Type": "application/json",
},
}
);
check(loginResponse, {
"login successful": (r) => r.status === 200,
"login returns token": (r) => r.json("token") !== undefined,
});
return loginResponse.json("token");
}
// Main test scenario
export default function () {
// Select random user for this iteration
const user = userPool[Math.floor(Math.random() * userPool.length)];
const token = authenticate(user);
if (!token) {
errorRate.add(1);
return;
}
// Test scenario 1: User profile operations
testUserProfile(token);
sleep(1); // Think time between operations
// Test scenario 2: Order creation workflow
testOrderCreation(token);
sleep(1);
// Test scenario 3: Data retrieval operations
testDataRetrieval(token);
sleep(1);
// Collect custom metrics
collectSystemMetrics();
}
function testUserProfile(token) {
const headers = {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
};
// Get user profile
const profileResponse = http.get(`${BASE_URL}/api/users/profile`, {
headers,
});
const profileChecks = check(profileResponse, {
"profile fetch successful": (r) => r.status === 200,
"profile response time acceptable": (r) => r.timings.duration < 200,
"profile contains required fields": (r) => {
const profile = r.json("user");
return profile && profile.id && profile.email;
},
});
if (!profileChecks) {
errorRate.add(1);
}
apiCalls.add(1);
responseTimeP95.add(profileResponse.timings.duration);
// Update profile
const updateData = {
firstName: `TestUser${Math.floor(Math.random() * 1000)}`,
bio: "Updated bio from load test",
};
const updateResponse = http.put(
`${BASE_URL}/api/users/profile`,
JSON.stringify(updateData),
{ headers }
);
check(updateResponse, {
"profile update successful": (r) => r.status === 200,
"profile update response time acceptable": (r) => r.timings.duration < 300,
});
apiCalls.add(1);
}
function testOrderCreation(token) {
const headers = {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
};
// Simulate realistic order data
const orderData = {
items: [
{
productId: `product_${Math.floor(Math.random() * 100)}`,
quantity: Math.floor(Math.random() * 5) + 1,
price: Math.floor(Math.random() * 10000) + 1000,
},
],
shippingAddress: {
street: "123 Test Street",
city: "Test City",
zipCode: "12345",
country: "US",
},
paymentMethod: "card_test_token",
};
const orderResponse = http.post(
`${BASE_URL}/api/orders`,
JSON.stringify(orderData),
{ headers }
);
const orderChecks = check(orderResponse, {
"order creation successful": (r) => r.status === 201,
"order creation response time acceptable": (r) => r.timings.duration < 1000,
"order response contains orderId": (r) => r.json("orderId") !== undefined,
"order total calculated correctly": (r) => {
const order = r.json();
return order.total && order.total > 0;
},
});
if (!orderChecks) {
errorRate.add(1);
}
apiCalls.add(1);
responseTimeP95.add(orderResponse.timings.duration);
// Follow up with order status check
if (orderResponse.status === 201) {
const orderId = orderResponse.json("orderId");
sleep(0.5); // Small delay before status check
const statusResponse = http.get(`${BASE_URL}/api/orders/${orderId}`, {
headers,
});
check(statusResponse, {
"order status fetch successful": (r) => r.status === 200,
"order status response time acceptable": (r) => r.timings.duration < 200,
});
apiCalls.add(1);
}
}
function testDataRetrieval(token) {
const headers = {
Authorization: `Bearer ${token}`,
};
// Test search functionality with various queries
const searchQueries = [
{ q: "test", limit: 20 },
{ q: "product", category: "electronics", limit: 10 },
{ sort: "created_at", order: "desc", limit: 50 },
];
const query = searchQueries[Math.floor(Math.random() * searchQueries.length)];
const queryString = Object.keys(query)
.map((key) => `${key}=${encodeURIComponent(query[key])}`)
.join("&");
const searchResponse = http.get(`${BASE_URL}/api/search?${queryString}`, {
headers,
});
check(searchResponse, {
"search successful": (r) => r.status === 200,
"search response time acceptable": (r) => r.timings.duration < 500,
"search returns results array": (r) => Array.isArray(r.json("results")),
"search includes pagination": (r) => {
const data = r.json();
return data.total !== undefined && data.page !== undefined;
},
});
apiCalls.add(1);
responseTimeP95.add(searchResponse.timings.duration);
}
function collectSystemMetrics() {
// In a real scenario, you'd collect these from your monitoring system
// This is a simplified example
// Simulate database connection count (would come from monitoring API)
const dbConnections = Math.floor(Math.random() * 20) + 40;
databaseConnections.add(dbConnections);
// Simulate memory usage (would come from monitoring API)
const memUsage = Math.floor(Math.random() * 100) + 200;
memoryUsage.add(memUsage);
}
// Stress testing scenario for peak load conditions
export function stressTest() {
// This runs when k6 is started with --config stress-test.json
const stressUser = userPool[0];
const token = authenticate(stressUser);
if (!token) return;
// Rapid-fire requests to test system limits
for (let i = 0; i < 5; i++) {
const response = http.get(`${BASE_URL}/api/health`, {
headers: { Authorization: `Bearer ${token}` },
});
check(response, {
"stress test request successful": (r) => r.status === 200,
"stress test response under 1s": (r) => r.timings.duration < 1000,
});
sleep(0.1); // Very short think time for stress testing
}
}
// Performance test teardown
export function teardown(data) {
// Cleanup test data if needed
console.log("Load test completed");
console.log(`Total API calls: ${apiCalls.value}`);
console.log(`Error rate: ${(errorRate.value * 100).toFixed(2)}%`);
}
Database performance testing for query optimization:
// ✅ Database performance testing and query optimization validation
// performance/database-performance.test.js
describe("Database Performance Tests", () => {
let db;
let performanceMonitor;
beforeAll(async () => {
db = await connectToTestDatabase();
performanceMonitor = new DatabasePerformanceMonitor(db);
});
afterAll(async () => {
await db.close();
});
describe("Query Performance Benchmarks", () => {
beforeAll(async () => {
// Setup realistic test data volumes
await seedLargeDataset();
});
it("user search queries perform under acceptable limits", async () => {
const testQueries = [
{ email: "test@example.com" },
{ firstName: "John", lastName: "Doe" },
{ department: "Engineering" },
{ role: "Developer", isActive: true },
{ createdAt: { $gte: new Date("2023-01-01") } },
];
for (const query of testQueries) {
const startTime = process.hrtime.bigint();
const results = await db.users.find(query).limit(20).toArray();
const endTime = process.hrtime.bigint();
const durationMs = Number(endTime - startTime) / 1000000;
// Performance assertion
expect(durationMs).toBeLessThan(100); // Under 100ms
expect(results.length).toBeLessThanOrEqual(20);
console.log(
`Query ${JSON.stringify(query)}: ${durationMs.toFixed(2)}ms`
);
}
});
it("prevents N+1 query problems in user with orders retrieval", async () => {
const queryCountBefore = await performanceMonitor.getQueryCount();
// Fetch users with their orders (potential N+1 problem)
const usersWithOrders = await db.users
.aggregate([
{ $match: { isActive: true } },
{ $limit: 10 },
{
$lookup: {
from: "orders",
localField: "id",
foreignField: "userId",
as: "orders",
},
},
])
.toArray();
const queryCountAfter = await performanceMonitor.getQueryCount();
const queriesExecuted = queryCountAfter - queryCountBefore;
// Should be 1 aggregation query, not 1 + N individual queries
expect(queriesExecuted).toBeLessThanOrEqual(2); // Allow for connection overhead
expect(usersWithOrders).toHaveLength(10);
expect(usersWithOrders[0].orders).toBeDefined();
});
it("handles large result set pagination efficiently", async () => {
const pageSize = 50;
const totalPages = 10;
for (let page = 0; page < totalPages; page++) {
const startTime = process.hrtime.bigint();
const results = await db.users
.find({})
.sort({ createdAt: -1 })
.skip(page * pageSize)
.limit(pageSize)
.toArray();
const endTime = process.hrtime.bigint();
const durationMs = Number(endTime - startTime) / 1000000;
// Performance should not degrade significantly with offset
expect(durationMs).toBeLessThan(150); // Allow slight increase for later pages
expect(results.length).toBeLessThanOrEqual(pageSize);
console.log(`Page ${page + 1}: ${durationMs.toFixed(2)}ms`);
}
});
it("index usage is optimal for common queries", async () => {
const commonQueries = [
{ email: "test@example.com" },
{ department: "Engineering", role: "Developer" },
{ isActive: true, createdAt: { $gte: new Date("2023-01-01") } },
];
for (const query of commonQueries) {
// Use explain to check index usage
const explanation = await db.users
.find(query)
.explain("executionStats");
// Verify index was used (not collection scan)
expect(explanation.executionStats.executionSuccess).toBe(true);
expect(explanation.executionStats.totalExamined).toBeLessThanOrEqual(
explanation.executionStats.totalReturned * 2
); // Reasonable selectivity
// Check for collection scan (should be avoided)
const hasCollectionScan =
explanation.executionStats.executionStages.stage === "COLLSCAN";
expect(hasCollectionScan).toBe(false);
console.log(
`Query ${JSON.stringify(query)} used index: ${
explanation.executionStats.executionStages.indexName
}`
);
}
});
});
describe("Connection Pool Performance", () => {
it("handles concurrent connections efficiently", async () => {
const concurrentOperations = [];
const operationCount = 50;
// Create many concurrent database operations
for (let i = 0; i < operationCount; i++) {
concurrentOperations.push(
db.users.findOne({ id: `user${i % 10}` }) // Reuse some queries for caching test
);
}
const startTime = Date.now();
const results = await Promise.all(concurrentOperations);
const endTime = Date.now();
const totalDuration = endTime - startTime;
// Should handle concurrent operations efficiently
expect(totalDuration).toBeLessThan(1000); // Under 1 second for 50 operations
expect(results).toHaveLength(operationCount);
// Verify connection pool didn't get exhausted
const poolStats = await performanceMonitor.getConnectionPoolStats();
expect(poolStats.available).toBeGreaterThan(0);
console.log(
`${operationCount} concurrent operations: ${totalDuration}ms`
);
});
});
async function seedLargeDataset() {
// Create realistic test data volumes for performance testing
const users = [];
const orders = [];
for (let i = 0; i < 10000; i++) {
users.push({
id: `user${i}`,
email: `user${i}@example.com`,
firstName: `FirstName${i}`,
lastName: `LastName${i}`,
department: ["Engineering", "Sales", "Marketing", "Support"][i % 4],
role: ["Developer", "Manager", "Analyst", "Lead"][i % 4],
isActive: i % 10 !== 0, // 90% active users
createdAt: new Date(
Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000
),
});
// Create 0-5 orders per user
const orderCount = Math.floor(Math.random() * 6);
for (let j = 0; j < orderCount; j++) {
orders.push({
id: `order${i}_${j}`,
userId: `user${i}`,
total: Math.floor(Math.random() * 50000) + 1000,
status: ["pending", "completed", "cancelled"][
Math.floor(Math.random() * 3)
],
createdAt: new Date(
Date.now() - Math.random() * 180 * 24 * 60 * 60 * 1000
),
});
}
}
await db.users.insertMany(users);
await db.orders.insertMany(orders);
// Create indexes for performance testing
await db.users.createIndex({ email: 1 });
await db.users.createIndex({ department: 1, role: 1 });
await db.users.createIndex({ isActive: 1, createdAt: -1 });
await db.orders.createIndex({ userId: 1 });
await db.orders.createIndex({ status: 1, createdAt: -1 });
}
});
class DatabasePerformanceMonitor {
constructor(db) {
this.db = db;
this.initialStats = null;
}
async getQueryCount() {
const stats = await this.db.admin().serverStatus();
return stats.opcounters.query + stats.opcounters.command;
}
async getConnectionPoolStats() {
const stats = await this.db.admin().serverStatus();
return {
available: stats.connections.available,
current: stats.connections.current,
totalCreated: stats.connections.totalCreated,
};
}
}
Test-Driven Development: Design Through Testing
Professional TDD Workflow Implementation
TDD cycle that improves code design through testing first:
// ✅ TDD example: Building a subscription billing service
// Following Red-Green-Refactor cycle
describe("SubscriptionBillingService TDD", () => {
// RED: Write failing test first
describe("calculateNextBillingAmount - RED Phase", () => {
it("should calculate prorated amount for mid-cycle upgrade", () => {
// Test defines the API we want
const billingService = new SubscriptionBillingService();
const result = billingService.calculateNextBillingAmount({
currentPlan: { price: 1000, name: "basic" }, // $10.00
newPlan: { price: 3000, name: "premium" }, // $30.00
daysRemaining: 15,
daysInCycle: 30,
});
// Test drives the design - what should the result look like?
expect(result).toEqual({
proratedAmount: 1000, // ($30 - $10) * (15/30) = $10.00
nextCycleAmount: 3000,
description: "Prorated upgrade from basic to premium",
effectiveDate: expect.any(Date),
});
// This test FAILS because SubscriptionBillingService doesn't exist yet
});
});
// GREEN: Write minimal code to make test pass
class SubscriptionBillingService {
calculateNextBillingAmount({
currentPlan,
newPlan,
daysRemaining,
daysInCycle,
}) {
const priceDifference = newPlan.price - currentPlan.price;
const proratedAmount = Math.round(
priceDifference * (daysRemaining / daysInCycle)
);
return {
proratedAmount,
nextCycleAmount: newPlan.price,
description: `Prorated upgrade from ${currentPlan.name} to ${newPlan.name}`,
effectiveDate: new Date(),
};
}
}
// RED: Add more specific test cases
describe("calculateNextBillingAmount - More RED Phase", () => {
let billingService;
beforeEach(() => {
billingService = new SubscriptionBillingService();
});
it("should handle downgrade with refund calculation", () => {
const result = billingService.calculateNextBillingAmount({
currentPlan: { price: 3000, name: "premium" },
newPlan: { price: 1000, name: "basic" },
daysRemaining: 10,
daysInCycle: 30,
});
expect(result).toEqual({
proratedAmount: -667, // Negative for refund: ($10 - $30) * (10/30)
nextCycleAmount: 1000,
description: "Prorated downgrade from premium to basic with refund",
effectiveDate: expect.any(Date),
isRefund: true,
});
// This test FAILS - our implementation doesn't handle downgrades
});
it("should validate plan data before calculation", () => {
expect(() => {
billingService.calculateNextBillingAmount({
currentPlan: null, // Invalid input
newPlan: { price: 3000, name: "premium" },
daysRemaining: 15,
daysInCycle: 30,
});
}).toThrow("Invalid plan data provided");
// This test FAILS - no validation implemented
});
it("should handle same-plan changes (no billing impact)", () => {
const result = billingService.calculateNextBillingAmount({
currentPlan: { price: 2000, name: "standard" },
newPlan: { price: 2000, name: "standard" },
daysRemaining: 15,
daysInCycle: 30,
});
expect(result).toEqual({
proratedAmount: 0,
nextCycleAmount: 2000,
description: "No billing change - same plan",
effectiveDate: expect.any(Date),
});
// This test FAILS - not handled in current implementation
});
});
// GREEN: Expand implementation to handle new test cases
class ImprovedSubscriptionBillingService {
calculateNextBillingAmount({
currentPlan,
newPlan,
daysRemaining,
daysInCycle,
}) {
// Validation (driven by test)
this.validateInput({ currentPlan, newPlan, daysRemaining, daysInCycle });
const priceDifference = newPlan.price - currentPlan.price;
// Handle same plan (driven by test)
if (priceDifference === 0) {
return {
proratedAmount: 0,
nextCycleAmount: newPlan.price,
description: "No billing change - same plan",
effectiveDate: new Date(),
};
}
const proratedAmount = Math.round(
priceDifference * (daysRemaining / daysInCycle)
);
const isRefund = proratedAmount < 0;
return {
proratedAmount,
nextCycleAmount: newPlan.price,
description: this.buildDescription(currentPlan, newPlan, isRefund),
effectiveDate: new Date(),
...(isRefund && { isRefund: true }),
};
}
validateInput({ currentPlan, newPlan, daysRemaining, daysInCycle }) {
if (!currentPlan || !newPlan) {
throw new Error("Invalid plan data provided");
}
if (
typeof currentPlan.price !== "number" ||
typeof newPlan.price !== "number"
) {
throw new Error("Plan prices must be numbers");
}
if (daysRemaining < 0 || daysInCycle <= 0) {
throw new Error("Invalid billing cycle data");
}
}
buildDescription(currentPlan, newPlan, isRefund) {
if (isRefund) {
return `Prorated downgrade from ${currentPlan.name} to ${newPlan.name} with refund`;
} else {
return `Prorated upgrade from ${currentPlan.name} to ${newPlan.name}`;
}
}
}
// REFACTOR: Improve code structure while keeping tests green
describe("calculateNextBillingAmount - REFACTOR Phase", () => {
let billingService;
beforeEach(() => {
billingService = new RefactoredSubscriptionBillingService();
});
it("maintains all existing functionality after refactor", async () => {
// All previous tests should still pass with refactored implementation
// Test upgrade scenario
const upgradeResult = billingService.calculateNextBillingAmount({
currentPlan: { price: 1000, name: "basic" },
newPlan: { price: 3000, name: "premium" },
daysRemaining: 15,
daysInCycle: 30,
});
expect(upgradeResult.proratedAmount).toBe(1000);
// Test downgrade scenario
const downgradeResult = billingService.calculateNextBillingAmount({
currentPlan: { price: 3000, name: "premium" },
newPlan: { price: 1000, name: "basic" },
daysRemaining: 10,
daysInCycle: 30,
});
expect(downgradeResult.isRefund).toBe(true);
expect(downgradeResult.proratedAmount).toBe(-667);
});
});
// Final refactored implementation with better structure
class RefactoredSubscriptionBillingService {
constructor(options = {}) {
this.roundingPrecision = options.roundingPrecision || 0; // Round to dollars
this.validator = new BillingInputValidator();
this.calculator = new ProrationCalculator(this.roundingPrecision);
this.descriptionBuilder = new BillingDescriptionBuilder();
}
calculateNextBillingAmount(billingData) {
// Validate input (single responsibility)
this.validator.validate(billingData);
// Calculate proration (single responsibility)
const proratedAmount = this.calculator.calculateProration(billingData);
// Build description (single responsibility)
const description = this.descriptionBuilder.buildDescription(
billingData.currentPlan,
billingData.newPlan,
proratedAmount < 0
);
return {
proratedAmount,
nextCycleAmount: billingData.newPlan.price,
description,
effectiveDate: new Date(),
...(proratedAmount < 0 && { isRefund: true }),
};
}
}
// Supporting classes (also developed through TDD)
class BillingInputValidator {
validate({ currentPlan, newPlan, daysRemaining, daysInCycle }) {
this.validatePlans(currentPlan, newPlan);
this.validateCycleData(daysRemaining, daysInCycle);
}
validatePlans(currentPlan, newPlan) {
if (!currentPlan || !newPlan) {
throw new Error("Invalid plan data provided");
}
if (
typeof currentPlan.price !== "number" ||
typeof newPlan.price !== "number"
) {
throw new Error("Plan prices must be numbers");
}
if (currentPlan.price < 0 || newPlan.price < 0) {
throw new Error("Plan prices cannot be negative");
}
}
validateCycleData(daysRemaining, daysInCycle) {
if (
typeof daysRemaining !== "number" ||
typeof daysInCycle !== "number"
) {
throw new Error("Billing cycle data must be numbers");
}
if (daysRemaining < 0 || daysInCycle <= 0) {
throw new Error("Invalid billing cycle data");
}
if (daysRemaining > daysInCycle) {
throw new Error("Days remaining cannot exceed cycle length");
}
}
}
class ProrationCalculator {
constructor(roundingPrecision = 0) {
this.roundingPrecision = roundingPrecision;
}
calculateProration({ currentPlan, newPlan, daysRemaining, daysInCycle }) {
if (currentPlan.price === newPlan.price) {
return 0; // No proration needed for same price
}
const priceDifference = newPlan.price - currentPlan.price;
const prorationFactor = daysRemaining / daysInCycle;
const proratedAmount = priceDifference * prorationFactor;
return this.roundAmount(proratedAmount);
}
roundAmount(amount) {
const multiplier = Math.pow(10, this.roundingPrecision);
return Math.round(amount * multiplier) / multiplier;
}
}
class BillingDescriptionBuilder {
buildDescription(currentPlan, newPlan, isRefund) {
if (currentPlan.price === newPlan.price) {
return "No billing change - same plan";
}
const action = isRefund ? "downgrade" : "upgrade";
const suffix = isRefund ? " with refund" : "";
return `Prorated ${action} from ${currentPlan.name} to ${newPlan.name}${suffix}`;
}
}
// TDD-driven edge cases and business rules
describe("Advanced TDD Scenarios", () => {
let billingService;
beforeEach(() => {
billingService = new RefactoredSubscriptionBillingService();
});
it("handles leap year february correctly", () => {
const result = billingService.calculateNextBillingAmount({
currentPlan: { price: 1000, name: "basic" },
newPlan: { price: 3000, name: "premium" },
daysRemaining: 14,
daysInCycle: 29, // Leap year February
});
// Expected: ($30 - $10) * (14/29) = $9.66
expect(result.proratedAmount).toBe(966); // Rounded to cents
});
it("handles annual billing cycles appropriately", () => {
const result = billingService.calculateNextBillingAmount({
currentPlan: { price: 120000, name: "annual-basic" }, // $1200/year
newPlan: { price: 360000, name: "annual-premium" }, // $3600/year
daysRemaining: 182, // ~6 months remaining
daysInCycle: 365,
});
// Expected: ($3600 - $1200) * (182/365) ≈ $1196
expect(result.proratedAmount).toBe(119670);
});
});
});
// TDD produces better API design through test-first thinking
describe("TDD Benefits Demonstration", () => {
it("forces you to think about API design first", () => {
// Test-first approach makes you consider:
// - What parameters does the function need?
// - What should it return?
// - How should errors be handled?
// - What edge cases exist?
expect(typeof SubscriptionBillingService).toBe("function");
expect(() => new SubscriptionBillingService()).not.toThrow();
});
it("ensures comprehensive error handling from the start", () => {
const billingService = new RefactoredSubscriptionBillingService();
// TDD ensures error cases are handled because tests require them
expect(() => {
billingService.calculateNextBillingAmount({
currentPlan: null,
newPlan: { price: 1000, name: "basic" },
daysRemaining: 15,
daysInCycle: 30,
});
}).toThrow("Invalid plan data provided");
});
it("creates self-documenting code through test descriptions", () => {
// Test names become living documentation of what the code should do
// Tests demonstrate how to use the API
// Tests capture business requirements in executable form
expect(true).toBe(true); // This test documents TDD benefits
});
});
Continuous Testing and CI/CD Integration
Automated Quality Gates and Pipeline Integration
CI/CD pipeline with comprehensive testing integration:
// ✅ GitHub Actions workflow for continuous testing
// .github/workflows/test.yml
name: Continuous Testing Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
NODE_VERSION: '18.x'
MONGODB_VERSION: '6.0'
jobs:
# Phase 1: Code Quality and Unit Tests
unit-tests:
runs-on: ubuntu-latest
services:
mongodb:
image: mongo:6.0
env:
MONGO_INITDB_ROOT_USERNAME: test
MONGO_INITDB_ROOT_PASSWORD: test
ports:
- 27017:27017
options: >-
--health-cmd "mongosh --eval 'db.runCommand(\"ping\").ok' --quiet"
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linting
run: npm run lint
- name: Run unit tests with coverage
run: npm run test:unit -- --coverage
env:
NODE_ENV: test
DATABASE_URL: mongodb://test:test@localhost:27017/test
REDIS_URL: redis://localhost:6379
JWT_SECRET: test-secret-key
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
flags: unittests
- name: Check coverage thresholds
run: |
if ! npx nyc check-coverage --lines 85 --functions 85 --branches 80; then
echo "Coverage thresholds not met"
exit 1
fi
# Phase 2: Integration Tests
integration-tests:
runs-on: ubuntu-latest
needs: unit-tests
services:
mongodb:
image: mongo:6.0
env:
MONGO_INITDB_ROOT_USERNAME: test
MONGO_INITDB_ROOT_PASSWORD: test
ports:
- 27017:27017
# Mock external services for integration testing
stripe-mock:
image: stripemock/stripe-mock:latest
ports:
- 12111:12111
mailhog:
image: mailhog/mailhog:latest
ports:
- 1025:1025
- 8025:8025
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Wait for services to be ready
run: |
# Wait for MongoDB
timeout 60 bash -c 'until nc -z localhost 27017; do sleep 1; done'
# Wait for Stripe Mock
timeout 60 bash -c 'until nc -z localhost 12111; do sleep 1; done'
# Wait for MailHog
timeout 60 bash -c 'until nc -z localhost 1025; do sleep 1; done'
- name: Run integration tests
run: npm run test:integration
env:
NODE_ENV: test
DATABASE_URL: mongodb://test:test@localhost:27017/test_integration
STRIPE_API_URL: http://localhost:12111
EMAIL_SMTP_HOST: localhost
EMAIL_SMTP_PORT: 1025
- name: Upload integration test results
uses: actions/upload-artifact@v3
if: always()
with:
name: integration-test-results
path: test-results/integration/
# Phase 3: API Contract Tests
contract-tests:
runs-on: ubuntu-latest
needs: unit-tests
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Start application
run: |
npm run build
npm start &
sleep 10 # Wait for app to start
env:
NODE_ENV: production
PORT: 3000
DATABASE_URL: mongodb://localhost:27017/contract_test
- name: Run API contract tests
run: npm run test:contract
- name: Generate API documentation
run: npm run generate:api-docs
- name: Upload API docs
uses: actions/upload-artifact@v3
with:
name: api-documentation
path: docs/api/
# Phase 4: Performance Tests
performance-tests:
runs-on: ubuntu-latest
needs: integration-tests
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install k6
run: |
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
- name: Start application for performance testing
run: |
npm ci
npm run build
npm start &
sleep 15 # Wait for app to fully start
env:
NODE_ENV: production
- name: Run performance tests
run: k6 run performance/load-tests/api-load-test.js
env:
BASE_URL: http://localhost:3000
- name: Check performance thresholds
run: |
# Performance tests will fail the job if thresholds are not met
echo "Performance tests completed successfully"
- name: Upload performance results
uses: actions/upload-artifact@v3
if: always()
with:
name: performance-test-results
path: performance-results/
# Phase 5: Security Tests
security-tests:
runs-on: ubuntu-latest
needs: unit-tests
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run dependency security audit
run: |
npm audit --audit-level=moderate
- name: Run Semgrep security scan
uses: returntocorp/semgrep-action@v1
with:
config: >-
p/javascript
p/nodejs
p/security-audit
p/owasp-top-ten
- name: Run CodeQL analysis
uses: github/codeql-action/analyze@v2
with:
languages: javascript
# Phase 6: End-to-End Tests (on staging environment)
e2e-tests:
runs-on: ubuntu-latest
needs: [integration-tests, contract-tests]
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop')
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Deploy to staging
run: |
# Deploy application to staging environment
echo "Deploying to staging environment"
# This would typically use your deployment tool
- name: Wait for staging deployment
run: |
# Wait for staging environment to be ready
timeout 300 bash -c 'until curl -f ${{ vars.STAGING_URL }}/health; do sleep 5; done'
- name: Install Playwright
run: |
npm ci
npx playwright install --with-deps
- name: Run E2E tests
run: npx playwright test
env:
BASE_URL: ${{ vars.STAGING_URL }}
TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}
- name: Upload E2E test results
uses: actions/upload-artifact@v3
if: always()
with:
name: e2e-test-results
path: test-results/e2e/
- name: Upload screenshots on failure
uses: actions/upload-artifact@v3
if: failure()
with:
name: e2e-screenshots
path: test-results/screenshots/
# Quality Gate: All tests must pass before merge
quality-gate:
runs-on: ubuntu-latest
needs: [unit-tests, integration-tests, contract-tests, security-tests]
if: always()
steps:
- name: Check test results
run: |
if [[ "${{ needs.unit-tests.result }}" != "success" ]]; then
echo "Unit tests failed"
exit 1
fi
if [[ "${{ needs.integration-tests.result }}" != "success" ]]; then
echo "Integration tests failed"
exit 1
fi
if [[ "${{ needs.contract-tests.result }}" != "success" ]]; then
echo "Contract tests failed"
exit 1
fi
if [[ "${{ needs.security-tests.result }}" != "success" ]]; then
echo "Security tests failed"
exit 1
fi
echo "All quality gates passed!"
Test environment management and data strategy:
// ✅ Test environment and data management system
// tests/utils/TestEnvironmentManager.js
class TestEnvironmentManager {
constructor() {
this.environments = {
unit: new UnitTestEnvironment(),
integration: new IntegrationTestEnvironment(),
e2e: new E2ETestEnvironment(),
};
this.dataManager = new TestDataManager();
this.cleanupTasks = [];
}
async setupEnvironment(type, config = {}) {
const environment = this.environments[type];
if (!environment) {
throw new Error(`Unknown environment type: ${type}`);
}
console.log(`Setting up ${type} test environment...`);
const setup = await environment.setup(config);
// Register cleanup task
this.cleanupTasks.push(() => environment.cleanup());
return setup;
}
async teardownAll() {
console.log("Cleaning up test environments...");
for (const cleanup of this.cleanupTasks.reverse()) {
try {
await cleanup();
} catch (error) {
console.warn("Cleanup error:", error.message);
}
}
this.cleanupTasks = [];
}
}
class UnitTestEnvironment {
async setup(config = {}) {
// Setup for isolated unit tests
const mockDatabase = new MockDatabase();
const mockServices = new MockExternalServices();
// Create test-specific configuration
const testConfig = {
database: mockDatabase,
externalServices: mockServices,
logLevel: "error", // Reduce noise in tests
enableMetrics: false,
...config,
};
return {
config: testConfig,
mocks: {
database: mockDatabase,
services: mockServices,
},
};
}
async cleanup() {
// Unit tests typically don't need cleanup
// Mocks are automatically garbage collected
}
}
class IntegrationTestEnvironment {
constructor() {
this.testDatabase = null;
this.testRedis = null;
this.mockServices = null;
}
async setup(config = {}) {
// Setup real database for integration tests
this.testDatabase = await this.createTestDatabase();
this.testRedis = await this.createTestRedis();
this.mockServices = new MockExternalServices();
// Setup test data
await this.seedTestData();
const testConfig = {
database: this.testDatabase,
redis: this.testRedis,
externalServices: this.mockServices,
environment: "test",
...config,
};
return {
config: testConfig,
database: this.testDatabase,
redis: this.testRedis,
mocks: this.mockServices,
};
}
async createTestDatabase() {
const { MongoMemoryServer } = require("mongodb-memory-server");
const { MongoClient } = require("mongodb");
const mongod = await MongoMemoryServer.create({
binary: { version: "6.0.0" },
});
const uri = mongod.getUri();
const client = new MongoClient(uri);
await client.connect();
const db = client.db("integration_test");
// Store cleanup reference
this.mongod = mongod;
this.mongoClient = client;
return db;
}
async createTestRedis() {
const Redis = require("redis");
const client = Redis.createClient({
host: process.env.REDIS_HOST || "localhost",
port: process.env.REDIS_PORT || 6379,
db: 15, // Use separate DB for tests
});
await client.connect();
await client.flushDb(); // Clear any existing test data
this.redisClient = client;
return client;
}
async seedTestData() {
// Create minimal test data needed for integration tests
const testUsers = [
{
id: "test-user-1",
email: "test1@example.com",
firstName: "Test",
lastName: "User",
role: "user",
isActive: true,
createdAt: new Date(),
},
{
id: "test-admin-1",
email: "admin@example.com",
firstName: "Admin",
lastName: "User",
role: "admin",
isActive: true,
createdAt: new Date(),
},
];
await this.testDatabase.collection("users").insertMany(testUsers);
// Create test API keys
const testApiKeys = [
{
id: "test-api-key-1",
userId: "test-user-1",
key: "test_key_123",
permissions: ["read", "write"],
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
},
];
await this.testDatabase.collection("api_keys").insertMany(testApiKeys);
}
async cleanup() {
if (this.redisClient) {
await this.redisClient.flushDb();
await this.redisClient.quit();
}
if (this.mongoClient) {
await this.mongoClient.close();
}
if (this.mongod) {
await this.mongod.stop();
}
}
}
class E2ETestEnvironment {
async setup(config = {}) {
// E2E tests typically run against staging/test environments
const baseUrl = process.env.E2E_BASE_URL || "http://localhost:3000";
// Ensure test environment is ready
await this.waitForEnvironment(baseUrl);
// Setup test data in target environment
await this.setupE2ETestData(baseUrl);
return {
baseUrl,
testUsers: this.getE2ETestUsers(),
config: {
headless: process.env.CI === "true",
slowMo: process.env.CI === "true" ? 0 : 100,
...config,
},
};
}
async waitForEnvironment(baseUrl, timeoutMs = 60000) {
const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) {
try {
const response = await fetch(`${baseUrl}/health`);
if (response.ok) {
console.log("E2E environment is ready");
return;
}
} catch (error) {
// Environment not ready yet
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
throw new Error(`E2E environment not ready after ${timeoutMs}ms`);
}
async setupE2ETestData(baseUrl) {
// Create test data via API calls
const testDataResponse = await fetch(`${baseUrl}/api/test/setup`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.E2E_ADMIN_TOKEN}`,
},
body: JSON.stringify({
createTestUsers: true,
createTestProducts: true,
resetTestData: true,
}),
});
if (!testDataResponse.ok) {
throw new Error("Failed to setup E2E test data");
}
}
getE2ETestUsers() {
return {
regular: {
email: "e2e-user@example.com",
password: "E2ETestPass123!",
},
admin: {
email: "e2e-admin@example.com",
password: "E2EAdminPass123!",
},
premium: {
email: "e2e-premium@example.com",
password: "E2EPremiumPass123!",
},
};
}
async cleanup() {
// Clean up E2E test data
if (process.env.E2E_BASE_URL) {
try {
await fetch(`${process.env.E2E_BASE_URL}/api/test/cleanup`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.E2E_ADMIN_TOKEN}`,
},
});
} catch (error) {
console.warn("Failed to cleanup E2E test data:", error.message);
}
}
}
}
// Test data management for consistent test scenarios
class TestDataManager {
constructor() {
this.fixtures = new Map();
this.loadFixtures();
}
loadFixtures() {
// Load test data fixtures
this.fixtures.set("users", [
{
id: "fixture-user-1",
email: "john.doe@example.com",
firstName: "John",
lastName: "Doe",
role: "user",
subscription: "basic",
},
{
id: "fixture-admin-1",
email: "jane.admin@example.com",
firstName: "Jane",
lastName: "Admin",
role: "admin",
subscription: "premium",
},
]);
this.fixtures.set("products", [
{
id: "fixture-product-1",
name: "Test Product",
price: 2999,
category: "electronics",
inStock: true,
},
]);
this.fixtures.set("orders", [
{
id: "fixture-order-1",
userId: "fixture-user-1",
productId: "fixture-product-1",
quantity: 2,
total: 5998,
status: "completed",
},
]);
}
getFixture(name) {
return this.fixtures.get(name) || [];
}
createTestUser(overrides = {}) {
const baseUser = {
id: `test-user-${Date.now()}`,
email: `test-${Date.now()}@example.com`,
firstName: "Test",
lastName: "User",
role: "user",
isActive: true,
createdAt: new Date(),
};
return { ...baseUser, ...overrides };
}
createTestOrder(userId, overrides = {}) {
const baseOrder = {
id: `test-order-${Date.now()}`,
userId,
total: Math.floor(Math.random() * 10000) + 1000,
status: "pending",
createdAt: new Date(),
};
return { ...baseOrder, ...overrides };
}
}
module.exports = { TestEnvironmentManager, TestDataManager };
Key Takeaways
Advanced backend testing completes the quality picture by validating not just correctness, but performance, resilience, and real-world integration scenarios. API testing catches contract changes, performance testing reveals bottlenecks, TDD improves code design, and continuous testing integrates quality gates into deployment pipelines.
The production-ready testing mindset:
- API testing validates contracts: Comprehensive API tests catch breaking changes, validate error scenarios, and ensure integration compatibility
- Performance testing reveals bottlenecks: Load testing and performance monitoring identify scalability issues before users encounter them
- TDD improves design: Test-driven development creates better APIs and more maintainable code through testing-first thinking
- Continuous testing prevents regressions: Automated quality gates in CI/CD pipelines ensure consistent quality as applications evolve
What distinguishes enterprise testing strategies:
- API testing that validates contracts, error handling, and real-world integration scenarios comprehensively
- Performance testing that identifies bottlenecks, validates scalability assumptions, and monitors system behavior under load
- TDD practices that improve code design, ensure comprehensive error handling, and create self-documenting test suites
- CI/CD integration with quality gates that prevent regressions and maintain code quality automatically
Series Conclusion
We’ve completed comprehensive backend testing covering unit testing fundamentals, integration validation, API contract testing, performance validation, test-driven development, and continuous testing integration. You now possess the complete testing toolkit for building systems that scale from initial development to enterprise production environments.
The complete backend testing architect’s mastery:
- Testing pyramid strategy with balanced unit, integration, and end-to-end testing that provides fast feedback and comprehensive coverage
- Unit and integration testing that validates business logic, component interactions, and database operations with sophisticated mocking
- API and performance testing that ensures contract compliance, reveals scalability bottlenecks, and validates real-world scenarios
- TDD and continuous testing that improves code design and integrates quality gates into automated deployment pipelines
What’s Next
With testing and quality assurance foundations complete, we move into the code quality and best practices phase of backend development. You’ll learn to implement clean code principles, establish team coding standards, create comprehensive documentation strategies, and build monitoring systems that maintain quality as applications and teams scale.
Testing ensures your code works correctly. The next phase ensures it remains maintainable, scalable, and operable as your system and organization grow. Quality through testing meets quality through practices to create systems that stand the test of time.
You’re no longer just writing tests—you’re architecting quality systems that catch problems early, prevent regressions, validate performance, and integrate seamlessly into continuous delivery pipelines. The testing foundation is complete. Now we ensure the code itself meets enterprise standards for maintainability and operational excellence.