Testing Transport Concepts
This example demonstrates the conceptual understanding of transports and how to test them using framework-agnostic patterns.
📦 Version: This example corresponds to SyntropyLog v0.7.0
Overview​
Transports are essentially spies - they capture and store log entries for inspection. This example shows how to:
- Understand transports as testing utilities
- Use framework-agnostic testing patterns
- Combine all testing concepts learned
- Test framework boilerplate functions
- Achieve high test coverage
Key Concepts​
Transports as Spies​
Transports are designed to capture log entries in memory for testing:
import { SpyTransport } from 'syntropylog/testing';
// Create a transport that captures logs
const spyTransport = new SpyTransport();
// Log entries are stored in memory
logger.info('User created', { userId: 123 });
// Inspect captured logs
const entries = spyTransport.getEntries();
expect(entries).toHaveLength(1);
expect(entries[0].message).toBe('User created');
Framework Agnostic Testing​
All testing patterns work with any framework:
// Vitest
const testHelper = createTestHelper(vi.fn);
// Jest
const testHelper = createTestHelper(jest.fn);
// Jasmine
const testHelper = createTestHelper(jasmine.createSpy);
Declarative Patterns​
Tests focus on behavior and outcomes:
it('should send notification when user is created', async () => {
// Arrange
const user = { id: 1, name: 'John' };
// Act
await notificationService.sendWelcomeNotification(user);
// Assert
expect(mockTransport.getEntries()).toContainEqual(
expect.objectContaining({
level: 'info',
message: 'Welcome notification sent',
userId: 1
})
);
});
Example Code​
Notification Service​
// src/index.ts
import { syntropyLog } from 'syntropylog';
export class NotificationService {
private logger = syntropyLog.getLogger();
async sendWelcomeNotification(user: any): Promise<void> {
this.logger.info('Sending welcome notification', { userId: user.id });
// Simulate notification logic
await this.delay(100);
this.logger.info('Welcome notification sent', {
userId: user.id,
email: user.email
});
}
async sendPasswordReset(user: any): Promise<void> {
this.logger.warn('Password reset requested', { userId: user.id });
// Simulate password reset logic
await this.delay(200);
this.logger.info('Password reset email sent', {
userId: user.id,
email: user.email
});
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Framework boilerplate functions for testing
export async function initializeSyntropyLog() {
await syntropyLog.init({
logger: {
serviceName: 'notification-service',
level: 'info',
},
});
return syntropyLog;
}
export async function shutdownSyntropyLog() {
await syntropyLog.shutdown();
return { success: true };
}
Testing Transport Concepts​
// tests/transports-concepts.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { createTestHelper } from 'syntropylog/testing';
import { NotificationService, initializeSyntropyLog, shutdownSyntropyLog } from '../src/index';
describe('Transport Concepts', () => {
const testHelper = createTestHelper(vi.fn);
let notificationService: NotificationService;
beforeEach(() => {
testHelper.beforeEach();
notificationService = new NotificationService();
});
afterEach(() => {
testHelper.afterEach();
});
describe('Transports as Spies', () => {
it('should capture log entries in memory', async () => {
const user = { id: 123, name: 'John', email: 'john@example.com' };
await notificationService.sendWelcomeNotification(user);
const entries = testHelper.mockSyntropyLog.logger.transports[0].getEntries();
expect(entries).toHaveLength(2);
expect(entries[0].message).toBe('Sending welcome notification');
expect(entries[1].message).toBe('Welcome notification sent');
});
it('should include metadata in log entries', async () => {
const user = { id: 456, name: 'Jane', email: 'jane@example.com' };
await notificationService.sendPasswordReset(user);
const entries = testHelper.mockSyntropyLog.logger.transports[0].getEntries();
expect(entries[0]).toMatchObject({
level: 'warn',
message: 'Password reset requested',
userId: 456
});
expect(entries[1]).toMatchObject({
level: 'info',
message: 'Password reset email sent',
userId: 456,
email: 'jane@example.com'
});
});
});
describe('Framework Agnostic Testing', () => {
it('should work with any testing framework', () => {
// This test demonstrates that the patterns work with any framework
// The actual framework is injected via createTestHelper(vi.fn)
expect(testHelper.mockSyntropyLog).toBeDefined();
expect(testHelper.mockSyntropyLog.logger).toBeDefined();
expect(testHelper.mockSyntropyLog.logger.transports).toHaveLength(1);
});
it('should provide spy functions for assertions', () => {
const logger = testHelper.mockSyntropyLog.logger;
logger.info('Test message');
// The spy functions work like native framework spies
expect(logger.info).toHaveBeenCalledWith('Test message');
expect(logger.info).toHaveBeenCalledTimes(1);
});
});
describe('Declarative Patterns', () => {
it('should focus on behavior, not implementation', async () => {
const user = { id: 789, name: 'Bob', email: 'bob@example.com' };
await notificationService.sendWelcomeNotification(user);
// Assert the outcome, not the internal implementation
const entries = testHelper.mockSyntropyLog.logger.transports[0].getEntries();
expect(entries).toContainEqual(
expect.objectContaining({
level: 'info',
message: 'Welcome notification sent',
userId: 789,
email: 'bob@example.com'
})
);
});
it('should test business logic, not framework internals', async () => {
const user = { id: 999, name: 'Alice', email: 'alice@example.com' };
await notificationService.sendPasswordReset(user);
// Test that the business logic works correctly
const entries = testHelper.mockSyntropyLog.logger.transports[0].getEntries();
const resetRequested = entries.find(e => e.message === 'Password reset requested');
const resetSent = entries.find(e => e.message === 'Password reset email sent');
expect(resetRequested).toBeDefined();
expect(resetSent).toBeDefined();
expect(resetRequested?.userId).toBe(999);
expect(resetSent?.userId).toBe(999);
});
});
describe('Combining All Patterns', () => {
it('should demonstrate comprehensive testing approach', async () => {
const users = [
{ id: 1, name: 'User1', email: 'user1@example.com' },
{ id: 2, name: 'User2', email: 'user2@example.com' }
];
// Test multiple operations
await Promise.all([
notificationService.sendWelcomeNotification(users[0]),
notificationService.sendPasswordReset(users[1])
]);
const entries = testHelper.mockSyntropyLog.logger.transports[0].getEntries();
// Verify all expected log entries
expect(entries).toHaveLength(4);
expect(entries).toContainEqual(
expect.objectContaining({
message: 'Sending welcome notification',
userId: 1
})
);
expect(entries).toContainEqual(
expect.objectContaining({
message: 'Password reset requested',
userId: 2
})
);
});
});
describe('Framework Boilerplate Testing', () => {
it('should test initialization function', async () => {
const result = await initializeSyntropyLog();
expect(result).toBeDefined();
expect(result.getLogger).toBeDefined();
});
it('should test shutdown function', async () => {
const result = await shutdownSyntropyLog();
expect(result).toEqual({ success: true });
});
});
});
Test Coverage​
This example achieves 100% test coverage:
Tests: 10 passed, 10 total
Snapshots: 0 total
Time: 2.1s
Ran all test suites.
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------
Key Takeaways​
1. Transports are Spies​
Transports capture log entries in memory for testing, just like spies capture function calls.
2. Framework Agnostic Testing​
All testing patterns work with any framework through spy function injection.
3. Declarative Patterns​
Tests focus on behavior and outcomes, not implementation details.
4. Comprehensive Coverage​
Test both business logic and framework boilerplate functions.
5. Zero External Dependencies​
No external services needed - everything runs in memory.
6. Silent Observer Philosophy​
Framework errors are reported but never interrupt the application flow.
Related Examples​
- Example 28: Vitest Testing - Basic testing patterns
- Example 29: Jest Testing - Jest-specific patterns
- Example 30: Redis Context - Redis testing patterns
- Example 31: Serializers Testing - Serializer testing patterns
Next Steps​
- Run the example:
cd 32-testing-transports-concepts && npm test
- Explore the code: Review the notification service and tests
- Apply to your project: Use these patterns in your own transport testing
- Extend the patterns: Add more complex notification scenarios
- Combine patterns: Use all testing concepts together in your projects