This document covers testing strategies, tools, and best practices for the Wallet Service.
The project uses a multi-layer testing approach:
| Test Type | Location | Purpose |
|---|---|---|
| Unit Tests | src/**/*.spec.ts |
Test individual functions and classes |
| E2E Tests | test/**/*.spec.ts |
Test API endpoints end-to-end |
npm run test
npm run test -- --watch
npm run test -- --coverage
npm run test -- src/lib/services/example.spec.ts
// example.spec.ts
import test from 'ava';
import { ExampleService } from './example.service';
test('should return expected value', async t => {
const service = new ExampleService();
const result = await service.getValue();
t.is(result, 'expected');
});
test('should throw on invalid input', async t => {
const service = new ExampleService();
await t.throwsAsync(
async () => service.process(null),
{ message: 'Invalid input' }
);
});
For reusable test setup, use helper functions instead of hooks when you need flexibility:
import test from 'ava';
// Setup function with customizable options
function createTestContext(options = {}) {
return {
service: new ExampleService(options),
mockData: { id: '123', name: 'Test' }
};
}
test('default configuration', async t => {
const ctx = createTestContext();
const result = await ctx.service.process(ctx.mockData);
t.truthy(result);
});
test('custom configuration', async t => {
const ctx = createTestContext({ debug: true });
const result = await ctx.service.process(ctx.mockData);
t.truthy(result.debugInfo);
});
For consistent setup across all tests:
import test from 'ava';
test.beforeEach(t => {
t.context = {
service: new ExampleService(),
testData: generateTestData()
};
});
test('first test', async t => {
const result = await t.context.service.process(t.context.testData);
t.truthy(result);
});
test('second test', async t => {
const result = await t.context.service.validate(t.context.testData);
t.true(result.isValid);
});
beforeEach() |
Setup Functions |
|---|---|
| Runs for all tests | Called only when needed |
| Same setup for every test | Customizable per test |
Automatic cleanup with afterEach |
Manual cleanup required |
| Better for consistent fixtures | Better for varied scenarios |
import test from 'ava';
import { FastifyRequest, FastifyReply } from 'fastify';
import PassesController from './passes.controller';
function createMockRequest(overrides = {}): Partial<FastifyRequest> {
return {
params: {},
query: {},
headers: {},
body: {},
...overrides
};
}
function createMockReply(): Partial<FastifyReply> {
const reply = {
statusCode: 200,
status: function(code: number) {
this.statusCode = code;
return this;
},
send: function(payload: unknown) {
return { statusCode: this.statusCode, payload };
}
};
return reply;
}
test('returns 401 without valid key', async t => {
const controller = new PassesController();
const request = createMockRequest({
query: { salesforceId: '123', type: 'apple' }
});
const reply = createMockReply();
await controller.send(request as FastifyRequest, reply as FastifyReply);
t.is(reply.statusCode, 401);
});
import test from 'ava';
import { DigitalMembershipService } from './digital-membership.service';
// Mock the modules
const mockGetMemberData = async (id: string) => ({
salesforceId: id,
name: 'Test Member',
memberships: [{ type: 'Premium' }]
});
test.beforeEach(t => {
// Replace module functions with mocks
t.context.service = new DigitalMembershipService();
// Setup mocks as needed
});
test('retrieves passes by salesforce ID', async t => {
const result = await t.context.service.retrieveApplePassesBySalesforceId('123');
t.truthy(result.items);
t.is(result.count, result.items.length);
});
// test/api.spec.ts
import test from 'ava';
import { App } from '../src/app';
let app: App;
test.before(async () => {
app = App.getInstance();
await app.listen();
});
test.after(async () => {
await app.close();
});
test('GET / returns 200', async t => {
const response = await fetch('http://localhost:3000/');
t.is(response.status, 200);
});
test('GET /passes/update requires serial number', async t => {
const response = await fetch('http://localhost:3000/passes/update/');
t.is(response.status, 404);
});
npm run test -- --coverage
Coverage reports are generated in the coverage/ directory.
| Metric | Target |
|---|---|
| Statements | > 80% |
| Branches | > 75% |
| Functions | > 80% |
| Lines | > 80% |
import test from 'ava';
// Create a mock Salesforce module
const mockSalesforce = {
getMemberData: async (id: string) => ({
salesforceId: id,
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com',
memberships: []
}),
getSalesforceIdBySerialNumber: async (serial: string) => '001ABC123'
};
test('uses member data from Salesforce', async t => {
const data = await mockSalesforce.getMemberData('001ABC123');
t.is(data.firstName, 'John');
t.is(data.email, 'john@example.com');
});
const mockAppleModule = {
createApplePass: async (memberData: any) => {
// Return a mock Buffer representing a .pkpass file
return Buffer.from('mock-pkpass-content');
}
};
✅ Test behavior, not implementation
// Good: Tests the outcome
test('returns member passes', async t => {
const result = await service.getPassesForMember('123');
t.is(result.count, 2);
});
✅ Use descriptive test names
test('returns 401 when authorization header is missing', async t => {
// ...
});
✅ Keep tests independent
test.beforeEach(t => {
// Fresh context for each test
t.context.service = new Service();
});
✅ Test edge cases
test('handles empty member list', async t => {
const result = await service.getPassesForMember('no-memberships');
t.is(result.count, 0);
t.deepEqual(result.items, []);
});
❌ Don't test external services directly
// Bad: Depends on Salesforce availability
test('fetches from Salesforce', async t => {
const data = await salesforce.query('SELECT Id FROM Account');
});
❌ Don't share state between tests
// Bad: Tests depend on each other
let sharedData;
test('first creates data', t => { sharedData = {...} });
test('second uses data', t => { /* uses sharedData */ });
❌ Don't use magic numbers/strings
// Bad
t.is(result.code, 200);
// Good
const HTTP_OK = 200;
t.is(result.code, HTTP_OK);
npm run test -- --verbose
npm run test -- --match="test name pattern"
Add a breakpoint and use the Debug Test launch configuration.
Tests run automatically on:
See bitbucket-pipelines.yml or equivalent CI configuration for details.