Testing Your Integration
Thorough testing ensures your ACP implementation works correctly and handles edge cases. This guide covers testing strategies, tools, and common scenarios.
Testing Strategy
┌─────────────────────────────────────────────────────────┐
│ Testing Pyramid │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌───────────┐ │
│ │ E2E │ ← Fewer, slower │
│ ┌─┴───────────┴─┐ │
│ │ Integration │ │
│ ┌─┴───────────────┴─┐ │
│ │ Unit Tests │ ← More, faster │
│ └───────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘Unit Tests
Testing Cart Calculations
// test/unit/cartService.test.js
const CartService = require('../../services/cartService');
describe('CartService', () => {
let cartService;
beforeEach(() => {
cartService = new CartService();
});
describe('calculateShipping', () => {
test('free shipping over $50', () => {
const shipping = cartService.calculateShipping(5500, { country: 'US' });
expect(shipping).toBe(0);
});
test('standard domestic shipping under $50', () => {
const shipping = cartService.calculateShipping(4000, { country: 'US' });
expect(shipping).toBe(500);
});
test('international shipping', () => {
const shipping = cartService.calculateShipping(4000, { country: 'CA' });
expect(shipping).toBe(2500);
});
});
describe('calculateTax', () => {
test('California tax rate', () => {
const tax = cartService.calculateTax(10000, { country: 'US', state: 'CA' });
expect(tax).toBe(725); // 7.25%
});
test('no tax for missing address', () => {
const tax = cartService.calculateTax(10000, null);
expect(tax).toBe(0);
});
});
});Testing Session Service
// test/unit/sessionService.test.js
const SessionService = require('../../services/sessionService');
describe('SessionService', () => {
describe('generateSessionId', () => {
test('generates valid format', () => {
const service = new SessionService();
const id = service.generateSessionId();
expect(id).toMatch(/^cs_[a-f0-9]{32}$/);
});
test('generates unique IDs', () => {
const service = new SessionService();
const ids = new Set();
for (let i = 0; i < 1000; i++) {
ids.add(service.generateSessionId());
}
expect(ids.size).toBe(1000);
});
});
});Testing Validation
// test/unit/validation.test.js
const { validateCreateSession } = require('../../middleware/validation');
describe('validateCreateSession', () => {
let req, res, next;
beforeEach(() => {
req = { body: {} };
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};
next = jest.fn();
});
test('rejects missing cart', () => {
validateCreateSession(req, res, next);
expect(res.status).toHaveBeenCalledWith(400);
expect(next).not.toHaveBeenCalled();
});
test('rejects empty cart', () => {
req.body = { cart: { items: [] } };
validateCreateSession(req, res, next);
expect(res.status).toHaveBeenCalledWith(400);
});
test('accepts valid cart', () => {
req.body = {
cart: {
items: [{ product_id: 'prod_001', quantity: 1 }]
}
};
validateCreateSession(req, res, next);
expect(next).toHaveBeenCalled();
});
});Integration Tests
Testing API Endpoints
// test/integration/checkout.test.js
const request = require('supertest');
const app = require('../../app');
const mongoose = require('mongoose');
describe('Checkout API', () => {
beforeAll(async () => {
await mongoose.connect(process.env.TEST_DATABASE_URL);
});
afterAll(async () => {
await mongoose.connection.close();
});
beforeEach(async () => {
// Clear test data
await mongoose.connection.db.dropDatabase();
});
describe('POST /acp/v1/checkout_sessions', () => {
test('creates session successfully', async () => {
const res = await request(app)
.post('/acp/v1/checkout_sessions')
.send({
cart: {
items: [{ product_id: 'prod_001', quantity: 2 }]
}
});
expect(res.status).toBe(201);
expect(res.body.checkout_session_id).toMatch(/^cs_/);
expect(res.body.state).toBe('not_ready_for_payment');
expect(res.body.cart.items).toHaveLength(1);
expect(res.body.totals.subtotal).toBe(5998);
});
test('creates ready session with address', async () => {
const res = await request(app)
.post('/acp/v1/checkout_sessions')
.send({
cart: {
items: [{ product_id: 'prod_001', quantity: 1 }]
},
buyer_context: {
email: 'test@example.com',
shipping_address: {
line1: '123 Main St',
city: 'San Francisco',
state: 'CA',
postal_code: '94102',
country: 'US'
}
}
});
expect(res.status).toBe(201);
expect(res.body.state).toBe('ready_for_payment');
});
test('rejects invalid product', async () => {
const res = await request(app)
.post('/acp/v1/checkout_sessions')
.send({
cart: {
items: [{ product_id: 'invalid_product', quantity: 1 }]
}
});
expect(res.status).toBe(400);
expect(res.body.error).toBe('Cart validation failed');
});
});
describe('GET /acp/v1/checkout_sessions/:id', () => {
test('retrieves existing session', async () => {
// Create session
const createRes = await request(app)
.post('/acp/v1/checkout_sessions')
.send({
cart: { items: [{ product_id: 'prod_001', quantity: 1 }] }
});
const sessionId = createRes.body.checkout_session_id;
// Retrieve session
const getRes = await request(app)
.get(`/acp/v1/checkout_sessions/${sessionId}`);
expect(getRes.status).toBe(200);
expect(getRes.body.checkout_session_id).toBe(sessionId);
});
test('returns 404 for unknown session', async () => {
const res = await request(app)
.get('/acp/v1/checkout_sessions/cs_nonexistent');
expect(res.status).toBe(404);
});
});
describe('PATCH /acp/v1/checkout_sessions/:id', () => {
test('updates buyer context', async () => {
// Create session
const createRes = await request(app)
.post('/acp/v1/checkout_sessions')
.send({
cart: { items: [{ product_id: 'prod_001', quantity: 1 }] }
});
const sessionId = createRes.body.checkout_session_id;
// Update session
const updateRes = await request(app)
.patch(`/acp/v1/checkout_sessions/${sessionId}`)
.send({
buyer_context: {
email: 'customer@example.com',
shipping_address: {
line1: '456 Oak Ave',
city: 'Los Angeles',
state: 'CA',
postal_code: '90001',
country: 'US'
}
}
});
expect(updateRes.status).toBe(200);
expect(updateRes.body.state).toBe('ready_for_payment');
expect(updateRes.body.buyer_context.email).toBe('customer@example.com');
});
test('recalculates totals on address change', async () => {
// Create session without address
const createRes = await request(app)
.post('/acp/v1/checkout_sessions')
.send({
cart: { items: [{ product_id: 'prod_001', quantity: 1 }] }
});
const initialTotal = createRes.body.totals.total;
const sessionId = createRes.body.checkout_session_id;
// Add address (triggers tax calculation)
const updateRes = await request(app)
.patch(`/acp/v1/checkout_sessions/${sessionId}`)
.send({
buyer_context: {
shipping_address: {
line1: '123 Main St',
city: 'San Francisco',
state: 'CA',
postal_code: '94102',
country: 'US'
}
}
});
expect(updateRes.body.totals.tax).toBeGreaterThan(0);
expect(updateRes.body.totals.total).toBeGreaterThan(initialTotal);
});
});
describe('POST /acp/v1/checkout_sessions/:id/complete', () => {
let sessionId;
beforeEach(async () => {
// Create ready session
const res = await request(app)
.post('/acp/v1/checkout_sessions')
.send({
cart: { items: [{ product_id: 'prod_001', quantity: 1 }] },
buyer_context: {
email: 'test@example.com',
shipping_address: {
line1: '123 Main St',
city: 'San Francisco',
state: 'CA',
postal_code: '94102',
country: 'US'
}
}
});
sessionId = res.body.checkout_session_id;
});
test('completes with valid payment token', async () => {
const res = await request(app)
.post(`/acp/v1/checkout_sessions/${sessionId}/complete`)
.send({
payment_token: 'pm_card_visa'
});
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
expect(res.body.order_id).toMatch(/^ord_/);
expect(res.body.session.state).toBe('completed');
});
test('rejects missing payment token', async () => {
const res = await request(app)
.post(`/acp/v1/checkout_sessions/${sessionId}/complete`)
.send({});
expect(res.status).toBe(400);
});
test('prevents double completion', async () => {
// Complete first time
await request(app)
.post(`/acp/v1/checkout_sessions/${sessionId}/complete`)
.send({ payment_token: 'pm_card_visa' });
// Try to complete again
const res = await request(app)
.post(`/acp/v1/checkout_sessions/${sessionId}/complete`)
.send({ payment_token: 'pm_card_visa' });
expect(res.status).toBe(400);
expect(res.body.current_state).toBe('completed');
});
});
});End-to-End Tests
Full Purchase Flow
// test/e2e/purchase.test.js
const request = require('supertest');
const app = require('../../app');
describe('Complete Purchase Flow', () => {
test('user can browse, add to cart, and checkout', async () => {
// 1. Browse products
const productsRes = await request(app)
.get('/acp/v1/products?search=t-shirt');
expect(productsRes.status).toBe(200);
expect(productsRes.body.products.length).toBeGreaterThan(0);
const product = productsRes.body.products[0];
// 2. Create checkout session
const createRes = await request(app)
.post('/acp/v1/checkout_sessions')
.send({
cart: {
items: [{ product_id: product.id, quantity: 2 }]
}
});
expect(createRes.status).toBe(201);
const sessionId = createRes.body.checkout_session_id;
// 3. Add shipping info
const updateRes = await request(app)
.patch(`/acp/v1/checkout_sessions/${sessionId}`)
.send({
buyer_context: {
email: 'customer@example.com',
shipping_address: {
line1: '123 Main St',
city: 'San Francisco',
state: 'CA',
postal_code: '94102',
country: 'US'
}
}
});
expect(updateRes.body.state).toBe('ready_for_payment');
// 4. Complete purchase
const completeRes = await request(app)
.post(`/acp/v1/checkout_sessions/${sessionId}/complete`)
.send({
payment_token: 'pm_card_visa'
});
expect(completeRes.status).toBe(200);
expect(completeRes.body.success).toBe(true);
// 5. Verify order created
const orderRes = await request(app)
.get(`/acp/v1/checkout_sessions/${sessionId}`);
expect(orderRes.body.state).toBe('completed');
expect(orderRes.body.order.order_id).toBeDefined();
});
});Testing Tools
Test Runner Configuration
// jest.config.js
module.exports = {
testEnvironment: 'node',
setupFilesAfterEnv: ['./test/setup.js'],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
testPathIgnorePatterns: ['/node_modules/'],
collectCoverageFrom: [
'services/**/*.js',
'routes/**/*.js',
'middleware/**/*.js'
]
};Test Setup
// test/setup.js
const mongoose = require('mongoose');
// Use in-memory database for tests
beforeAll(async () => {
const { MongoMemoryServer } = require('mongodb-memory-server');
const mongoServer = await MongoMemoryServer.create();
await mongoose.connect(mongoServer.getUri());
});
afterAll(async () => {
await mongoose.disconnect();
});
// Reset database between tests
afterEach(async () => {
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany({});
}
});Manual Testing Checklist
Product Feed
- All products return correctly
- Search filters work
- Pagination works
- Single product endpoint works
- Out of stock items handled
Checkout Sessions
- Session creates with valid cart
- Invalid products rejected
- Session state updates correctly
- Totals calculate accurately
- Tax applies per state
- Shipping calculates correctly
- Session expires after 30 min
Payments
- Valid card succeeds
- Declined card handled
- Expired card handled
- Insufficient funds handled
- Idempotency prevents duplicates
Error Handling
- 404 for missing resources
- 400 for invalid requests
- Clear error messages
- No sensitive data leaked
Load Testing
// loadtest/checkout.js
const autocannon = require('autocannon');
const instance = autocannon({
url: 'http://localhost:3000/acp/v1/products',
connections: 100,
duration: 30,
headers: {
'Content-Type': 'application/json'
}
}, (err, result) => {
if (err) {
console.error(err);
return;
}
console.log(result);
});
autocannon.track(instance);📊
Target metrics:
- Product feed: < 100ms p95
- Create session: < 200ms p95
- Complete checkout: < 500ms p95
Continuous Integration
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
mongodb:
image: mongo:6
ports:
- 27017:27017
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test -- --coverage
env:
TEST_DATABASE_URL: mongodb://localhost:27017/test
STRIPE_SECRET_KEY: sk_test_xxx
- name: Upload coverage
uses: codecov/codecov-action@v3Summary
You’ve now learned to:
- ✅ Write unit tests for services
- ✅ Create integration tests for API endpoints
- ✅ Build end-to-end tests for complete flows
- ✅ Set up test infrastructure
- ✅ Run manual testing checklists
- ✅ Implement CI/CD testing
Your ACP integration is ready for production!
Next Steps
- API Reference - Complete API documentation
- Security Guide - Production security
- Deployment - Going live