Testing Guide ​
This comprehensive guide covers testing strategies, patterns, and best practices when working with the ziegel library ecosystem.
Overview ​
ziegel libraries are designed with testability in mind, providing clean APIs, dependency injection support, and comprehensive testing utilities. This guide covers unit testing, integration testing, and end-to-end testing patterns.
Testing Framework Setup ​
Vitest Configuration ​
All ziegel libraries use Vitest as the testing framework. Here's the recommended configuration:
typescript
// vitest.config.mjs
import { defineConfig } from 'vitest/config';
export default defineConfig({
root: './src',
test: {
include: ['**/*.{[sS]pec,[tT]est}.?(c|m)[jt]s?(x)'],
coverage: {
reporter: ['text', 'json', 'html'],
threshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
},
environment: 'node'
}
});Test Environment Setup ​
typescript
// tests/setup.ts
import { beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
import { Container } from '@breadstone/ziegel-platform';
beforeAll(() => {
// Global test setup
Container.configure(container => {
container.registerSingleton('ILogger', MockLogger);
container.registerSingleton('IHttpClient', MockHttpClient);
});
});
afterAll(() => {
// Global cleanup
Container.clear();
});
beforeEach(() => {
// Per-test setup
MockLogger.reset();
MockHttpClient.reset();
});
afterEach(() => {
// Per-test cleanup
});Unit Testing Patterns ​
Testing Core Utilities ​
typescript
import { describe, it, expect } from 'vitest';
import { Guid, DateTime, ArgumentException } from '@breadstone/ziegel-core';
describe('Guid', () => {
it('should generate unique identifiers', () => {
const guid1 = Guid.newGuid();
const guid2 = Guid.newGuid();
expect(guid1).not.toBe(guid2);
expect(Guid.isValid(guid1.toString())).toBe(true);
});
it('should validate GUID strings correctly', () => {
expect(Guid.isValid('550e8400-e29b-41d4-a716-446655440000')).toBe(true);
expect(Guid.isValid('invalid-guid')).toBe(false);
expect(Guid.isValid('')).toBe(false);
});
it('should parse valid GUID strings', () => {
const guidString = '550e8400-e29b-41d4-a716-446655440000';
const guid = Guid.parse(guidString);
expect(guid.toString()).toBe(guidString);
});
it('should throw for invalid GUID strings', () => {
expect(() => Guid.parse('invalid-guid'))
.toThrow(ArgumentException);
});
});
describe('DateTime', () => {
it('should create current date time', () => {
const now = DateTime.now();
const jsNow = new Date();
expect(Math.abs(now.getTime() - jsNow.getTime())).toBeLessThan(1000);
});
it('should add time periods correctly', () => {
const date = new DateTime(2023, 1, 1);
const future = date.addDays(30);
expect(future.day).toBe(31);
expect(future.month).toBe(1);
});
it('should format dates correctly', () => {
const date = new DateTime(2023, 6, 15, 14, 30, 0);
expect(date.toString('yyyy-MM-dd')).toBe('2023-06-15');
expect(date.toString('HH:mm:ss')).toBe('14:30:00');
});
});Testing Data Layer ​
typescript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { Repository, IQueryBuilder, QueryOperator } from '@breadstone/ziegel-data';
interface User {
id: string;
name: string;
email: string;
active: boolean;
}
describe('Repository', () => {
let mockHttpClient: any;
let repository: Repository<User>;
beforeEach(() => {
mockHttpClient = {
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn()
};
repository = new Repository<User>('/api/users', mockHttpClient);
});
it('should fetch entity by id', async () => {
const user = { id: '1', name: 'John Doe', email: 'john@example.com', active: true };
mockHttpClient.get.mockResolvedValue({ data: user });
const result = await repository.getById('1');
expect(mockHttpClient.get).toHaveBeenCalledWith('/api/users/1');
expect(result).toEqual(user);
});
it('should handle not found errors', async () => {
mockHttpClient.get.mockRejectedValue(new Error('Not Found'));
await expect(repository.getById('999'))
.rejects.toThrow('Not Found');
});
it('should create new entities', async () => {
const newUser = { id: '2', name: 'Jane Doe', email: 'jane@example.com', active: true };
mockHttpClient.post.mockResolvedValue({ data: newUser });
const result = await repository.add(newUser);
expect(mockHttpClient.post).toHaveBeenCalledWith('/api/users', newUser);
expect(result).toEqual(newUser);
});
it('should build complex queries', () => {
const queryBuilder = repository.query()
.where('active', QueryOperator.Equals, true)
.and('name', QueryOperator.Contains, 'John')
.orderBy('email')
.take(10);
const queryString = queryBuilder.toQueryString();
expect(queryString).toContain('active=true');
expect(queryString).toContain('name__contains=John');
expect(queryString).toContain('orderBy=email');
expect(queryString).toContain('take=10');
});
});Testing HTTP Client ​
typescript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { HttpClient, HttpInterceptor } from '@breadstone/ziegel-platform-http';
describe('HttpClient', () => {
let httpClient: HttpClient;
let mockFetch: any;
beforeEach(() => {
mockFetch = vi.fn();
global.fetch = mockFetch;
httpClient = new HttpClient({ baseUrl: 'https://api.example.com' });
});
it('should make GET requests', async () => {
const mockResponse = { data: 'test' };
mockFetch.mockResolvedValue({
ok: true,
status: 200,
json: () => Promise.resolve(mockResponse)
});
const response = await httpClient.get('/test');
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/test',
expect.objectContaining({
method: 'GET'
})
);
expect(response.data).toEqual(mockResponse);
});
it('should handle request errors', async () => {
mockFetch.mockRejectedValue(new Error('Network Error'));
await expect(httpClient.get('/test'))
.rejects.toThrow('Network Error');
});
it('should apply interceptors', async () => {
const authInterceptor: HttpInterceptor = {
request: (request) => {
request.headers['Authorization'] = 'Bearer token123';
return request;
}
};
httpClient.addInterceptor(authInterceptor);
mockFetch.mockResolvedValue({
ok: true,
status: 200,
json: () => Promise.resolve({})
});
await httpClient.get('/test');
expect(mockFetch).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
headers: expect.objectContaining({
'Authorization': 'Bearer token123'
})
})
);
});
});Testing Reactive Patterns ​
typescript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { Subject, Observable } from '@breadstone/ziegel-rx';
describe('Observable Patterns', () => {
let subject: Subject<number>;
beforeEach(() => {
subject = new Subject<number>();
});
it('should emit values to subscribers', () => {
const values: number[] = [];
const subscription = subject.subscribe(value => values.push(value));
subject.next(1);
subject.next(2);
subject.next(3);
expect(values).toEqual([1, 2, 3]);
subscription.unsubscribe();
});
it('should support operators', () => {
const evenNumbers: number[] = [];
const subscription = subject
.pipe(
filter(x => x % 2 === 0),
map(x => x * 2)
)
.subscribe(value => evenNumbers.push(value));
subject.next(1); // filtered out
subject.next(2); // becomes 4
subject.next(3); // filtered out
subject.next(4); // becomes 8
expect(evenNumbers).toEqual([4, 8]);
subscription.unsubscribe();
});
it('should handle errors', () => {
const errors: any[] = [];
const subscription = subject.subscribe({
next: () => {},
error: (error) => errors.push(error)
});
const testError = new Error('Test error');
subject.error(testError);
expect(errors).toContain(testError);
subscription.unsubscribe();
});
});Integration Testing ​
Testing Service Integration ​
typescript
import { describe, it, expect, beforeEach } from 'vitest';
import { UserService } from '../services/UserService';
import { MockUserRepository } from '../mocks/MockUserRepository';
import { MockCacheService } from '../mocks/MockCacheService';
describe('UserService Integration', () => {
let userService: UserService;
let mockRepository: MockUserRepository;
let mockCache: MockCacheService;
beforeEach(() => {
mockRepository = new MockUserRepository();
mockCache = new MockCacheService();
userService = new UserService(mockRepository, mockCache);
});
it('should cache user data on first load', async () => {
const user = { id: '1', name: 'John Doe', email: 'john@example.com' };
mockRepository.setUser('1', user);
// First call - should hit repository
const result1 = await userService.getUser('1');
expect(result1).toEqual(user);
expect(mockRepository.getCallCount('getById')).toBe(1);
// Second call - should hit cache
const result2 = await userService.getUser('1');
expect(result2).toEqual(user);
expect(mockRepository.getCallCount('getById')).toBe(1); // Still 1
expect(mockCache.getCallCount('get')).toBe(1);
});
it('should invalidate cache on user update', async () => {
const user = { id: '1', name: 'John Doe', email: 'john@example.com' };
const updatedUser = { ...user, name: 'John Smith' };
mockRepository.setUser('1', user);
// Load user (caches it)
await userService.getUser('1');
// Update user
mockRepository.setUser('1', updatedUser);
await userService.updateUser(updatedUser);
// Should load fresh data
const result = await userService.getUser('1');
expect(result.name).toBe('John Smith');
expect(mockCache.getCallCount('delete')).toBe(1);
});
});Testing with Dependency Injection ​
typescript
import { describe, it, expect, beforeEach } from 'vitest';
import { Container, IContainer } from '@breadstone/ziegel-platform';
import { OrderService } from '../services/OrderService';
describe('OrderService with DI', () => {
let container: IContainer;
let orderService: OrderService;
beforeEach(() => {
container = new Container();
// Register test dependencies
container.registerSingleton('IUserRepository', MockUserRepository);
container.registerSingleton('IOrderRepository', MockOrderRepository);
container.registerSingleton('IPaymentService', MockPaymentService);
container.registerSingleton('INotificationService', MockNotificationService);
orderService = container.resolve<OrderService>('OrderService');
});
it('should process order with all dependencies', async () => {
const orderRequest = {
userId: '1',
items: [{ productId: 'p1', quantity: 2, price: 10.00 }]
};
const result = await orderService.processOrder(orderRequest);
expect(result).toBeDefined();
expect(result.status).toBe('Confirmed');
// Verify all services were called
const paymentService = container.resolve<MockPaymentService>('IPaymentService');
const notificationService = container.resolve<MockNotificationService>('INotificationService');
expect(paymentService.processPaymentCalled).toBe(true);
expect(notificationService.sendConfirmationCalled).toBe(true);
});
});Mock Objects and Test Utilities ​
Creating Effective Mocks ​
typescript
// MockHttpClient.ts
export class MockHttpClient implements IHttpClient {
private responses = new Map<string, any>();
private callLog: Array<{ method: string; url: string; data?: any }> = [];
setResponse(method: string, url: string, response: any): void {
this.responses.set(`${method}:${url}`, response);
}
getCallLog(): Array<{ method: string; url: string; data?: any }> {
return [...this.callLog];
}
async get<T>(url: string): Promise<HttpResponse<T>> {
this.callLog.push({ method: 'GET', url });
const response = this.responses.get(`GET:${url}`);
if (!response) {
throw new Error(`No mock response configured for GET ${url}`);
}
return response;
}
async post<T>(url: string, data: any): Promise<HttpResponse<T>> {
this.callLog.push({ method: 'POST', url, data });
const response = this.responses.get(`POST:${url}`);
if (!response) {
throw new Error(`No mock response configured for POST ${url}`);
}
return response;
}
reset(): void {
this.responses.clear();
this.callLog.length = 0;
}
}Test Data Builders ​
typescript
// UserBuilder.ts
export class UserBuilder {
private user: Partial<User> = {};
static aUser(): UserBuilder {
return new UserBuilder()
.withId(Guid.newGuid().toString())
.withName('John Doe')
.withEmail('john.doe@example.com')
.withActive(true);
}
withId(id: string): UserBuilder {
this.user.id = id;
return this;
}
withName(name: string): UserBuilder {
this.user.name = name;
return this;
}
withEmail(email: string): UserBuilder {
this.user.email = email;
return this;
}
withActive(active: boolean): UserBuilder {
this.user.active = active;
return this;
}
build(): User {
return this.user as User;
}
}
// Usage in tests
describe('User validation', () => {
it('should accept valid users', () => {
const user = UserBuilder.aUser()
.withEmail('test@example.com')
.build();
expect(isValidUser(user)).toBe(true);
});
it('should reject inactive users', () => {
const user = UserBuilder.aUser()
.withActive(false)
.build();
expect(isValidUser(user)).toBe(false);
});
});End-to-End Testing ​
API Testing ​
typescript
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { TestServer } from './TestServer';
import { HttpClient } from '@breadstone/ziegel-platform-http';
describe('User API E2E', () => {
let testServer: TestServer;
let httpClient: HttpClient;
beforeAll(async () => {
testServer = new TestServer();
await testServer.start();
httpClient = new HttpClient({
baseUrl: `http://localhost:${testServer.port}`
});
});
afterAll(async () => {
await testServer.stop();
});
it('should create and retrieve user', async () => {
const newUser = {
name: 'Test User',
email: 'test@example.com'
};
// Create user
const createResponse = await httpClient.post('/api/users', newUser);
expect(createResponse.status).toBe(201);
expect(createResponse.data.id).toBeDefined();
// Retrieve user
const userId = createResponse.data.id;
const getResponse = await httpClient.get(`/api/users/${userId}`);
expect(getResponse.status).toBe(200);
expect(getResponse.data.name).toBe(newUser.name);
expect(getResponse.data.email).toBe(newUser.email);
});
it('should handle validation errors', async () => {
const invalidUser = {
name: '',
email: 'invalid-email'
};
try {
await httpClient.post('/api/users', invalidUser);
expect.fail('Should have thrown validation error');
} catch (error: any) {
expect(error.status).toBe(400);
expect(error.data.errors).toBeDefined();
}
});
});Performance Testing ​
Load Testing Services ​
typescript
import { describe, it, expect } from 'vitest';
import { performance } from 'perf_hooks';
describe('Performance Tests', () => {
it('should process large datasets efficiently', async () => {
const largeDataset = Array.from({ length: 10000 }, (_, i) => ({
id: i.toString(),
value: Math.random()
}));
const start = performance.now();
const result = await processLargeDataset(largeDataset);
const end = performance.now();
const duration = end - start;
expect(result.length).toBe(largeDataset.length);
expect(duration).toBeLessThan(1000); // Should complete in under 1 second
});
it('should handle concurrent requests', async () => {
const concurrentRequests = 50;
const promises = Array.from({ length: concurrentRequests }, () =>
userService.getUser('test-user-id')
);
const start = performance.now();
const results = await Promise.all(promises);
const end = performance.now();
const duration = end - start;
expect(results.length).toBe(concurrentRequests);
expect(results.every(r => r !== null)).toBe(true);
expect(duration).toBeLessThan(5000); // Should complete in under 5 seconds
});
});Test Organization Best Practices ​
Project Structure ​
src/
├── services/
│ └── UserService.ts
├── repositories/
│ └── UserRepository.ts
└── models/
└── User.ts
tests/
├── unit/
│ ├── services/
│ │ └── UserService.test.ts
│ └── repositories/
│ └── UserRepository.test.ts
├── integration/
│ └── UserService.integration.test.ts
├── e2e/
│ └── UserAPI.e2e.test.ts
├── mocks/
│ ├── MockUserRepository.ts
│ └── MockHttpClient.ts
├── builders/
│ └── UserBuilder.ts
└── setup.tsTest Naming Conventions ​
typescript
describe('UserService', () => {
describe('getUser', () => {
it('should return user when valid id provided', () => {});
it('should throw NotFoundException when user not found', () => {});
it('should return cached user on subsequent calls', () => {});
});
describe('createUser', () => {
it('should create user with valid data', () => {});
it('should throw ValidationException with invalid email', () => {});
it('should generate unique id for new user', () => {});
});
});Continuous Integration ​
GitHub Actions Configuration ​
yaml
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- run: npm install
- run: npm run test
- run: npm run test:coverage
- name: Upload coverage reports
uses: codecov/codecov-action@v3
with:
file: ./coverage/coverage-final.jsonTest Scripts ​
json
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui",
"test:unit": "vitest run tests/unit",
"test:integration": "vitest run tests/integration",
"test:e2e": "vitest run tests/e2e"
}
}Debugging Tests ​
VS Code Configuration ​
json
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Vitest Tests",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/vitest/vitest.mjs",
"args": ["run", "--reporter=verbose"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}Test Debugging Tips ​
typescript
// Use test.only to run specific tests
describe.only('UserService', () => {
it.only('should debug this specific test', () => {
// Add breakpoints and debug
debugger;
const result = userService.getUser('1');
console.log('Debug result:', result);
});
});
// Use test.skip to temporarily disable tests
describe('UserService', () => {
it.skip('should skip this flaky test', () => {
// Temporarily disabled
});
});Common Testing Patterns ​
Testing Async Code ​
typescript
it('should handle promises correctly', async () => {
const result = await asyncOperation();
expect(result).toBeDefined();
});
it('should handle promise rejections', async () => {
await expect(failingAsyncOperation())
.rejects.toThrow('Expected error message');
});Testing Timers ​
typescript
import { vi } from 'vitest';
it('should handle timeouts', async () => {
vi.useFakeTimers();
const promise = delayedOperation(1000);
vi.advanceTimersByTime(1000);
const result = await promise;
expect(result).toBe('completed');
vi.useRealTimers();
});Testing Event Emitters ​
typescript
it('should emit events correctly', (done) => {
const emitter = new EventEmitter();
emitter.on('test-event', (data) => {
expect(data).toBe('test-data');
done();
});
emitter.emit('test-event', 'test-data');
});This testing guide provides comprehensive coverage of testing patterns and best practices for ziegel applications. Always remember to test behavior, not implementation, and aim for high code coverage while maintaining test quality.