Testing Patterns with Jest
This example demonstrates how to write declarative, behavior-focused tests for SyntropyLog applications using Jest. The key insight is to avoid testing the framework itself and instead focus on testing your business logic.
๐ฏ What You'll Learnโ
- How to use
SyntropyLogMock
to avoid framework initialization issues - How to write tests that focus on behavior, not implementation
- How to use Jest-specific features with SyntropyLog
- How to avoid testing external dependencies (Redis, brokers, etc.)
- How to create maintainable and readable tests
- How the mock simulates all framework functionality in memory
๐ Quick Startโ
- Install Dependencies
- Run Tests
- Watch Mode
- Coverage
npm install
npm test
npm run test:watch
npm run test:coverage
๐ Prerequisitesโ
- Node.js 18+ (using nvm:
source ~/.nvm/nvm.sh
) - npm or yarn
- Basic knowledge of Jest and TypeScript
๐ง Setupโ
1. Install Dependenciesโ
npm install syntropylog jest ts-jest @types/jest typescript
2. Configure Jestโ
Create jest.config.js
:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/tests'],
testMatch: ['**/*.test.ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!tests/**/*'
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
setupFilesAfterEnv: [],
moduleFileExtensions: ['ts', 'js', 'json']
};
3. Configure TypeScriptโ
Create tsconfig.json
:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"types": ["jest", "node"]
},
"include": ["src/**/*", "tests/**/*"],
"exclude": ["node_modules", "dist"]
}
๐ง Understanding the SyntropyLogMockโ
The SyntropyLogMock
is a complete simulation of the SyntropyLog framework that runs entirely in memory. Here's what it provides:
What the Mock Simulatesโ
- Logger
- Context Manager
- Other Managers
// Mock logger with all standard methods
const logger = mockSyntropyLog.getLogger('service-name');
logger.info('Message', { metadata: 'value' });
logger.warn('Warning message');
logger.error('Error message');
// Mock context manager with correlation IDs
const contextManager = mockSyntropyLog.getContextManager();
await contextManager.run(async () => {
contextManager.set('x-correlation-id', 'test-id');
// Your business logic here
});
// Mock HTTP, Broker, and Serialization managers
const httpManager = mockSyntropyLog.getHttpManager();
const brokerManager = mockSyntropyLog.getBrokerManager();
const serializationManager = mockSyntropyLog.getSerializationManager();
Why Use the Mock?โ
- โ
No Initialization: No need to call
syntropyLog.init()
orsyntropyLog.shutdown()
- โ No External Dependencies: No Redis, brokers, or HTTP servers needed
- โ Fast Tests: Everything runs in memory
- โ Reliable: No network issues or state conflicts between tests
- โ Isolated: Each test gets a fresh mock instance
๐งช Testing Patternsโ
1. Basic Test Setup with SyntropyLogMockโ
import { UserService } from '../src/index';
const { createTestHelper } = require('syntropylog/testing');
// Create test helper - this creates a SyntropyLogMock that simulates the entire framework
// No real initialization/shutdown needed - everything is in memory
const testHelper = createTestHelper();
describe('UserService', () => {
let userService: UserService;
beforeEach(() => {
testHelper.beforeEach(); // Reset mocks and create fresh instances
userService = new UserService(testHelper.mockSyntropyLog); // Inject the mock
});
// Test that demonstrates the mock is working
it('should use SyntropyLogMock instead of real framework', () => {
// Verify that we're using the mock, not the real framework
expect(testHelper.mockSyntropyLog).toBeDefined();
expect(typeof testHelper.mockSyntropyLog.getLogger).toBe('function');
expect(typeof testHelper.mockSyntropyLog.getContextManager).toBe('function');
// Verify the service is using the injected mock
expect(userService).toBeInstanceOf(UserService);
});
it('should create user successfully', async () => {
// Arrange
const userData = { name: 'John Doe', email: 'john@example.com' };
// Act
const result = await userService.createUser(userData);
// Assert
expect(result).toHaveProperty('userId');
expect(result.name).toBe('John Doe');
});
});
2. Alternative: Service Helperโ
For even simpler setup, use the service helper:
it('should create user with service helper', async () => {
// Arrange
const userData = { name: 'John Doe', email: 'john@example.com' };
// Act - Create service with mock in one line
const { createServiceWithMock, createSyntropyLogMock } = require('syntropylog/testing');
const userService = createServiceWithMock(UserService, createSyntropyLogMock());
const result = await userService.createUser(userData);
// Assert
expect(result).toHaveProperty('userId');
});
๐ฏ Key Principlesโ
1. Test Behavior, Not Implementationโ
- โ Good
- โ Avoid
Test what the system produces
it('should return user with correct data', async () => {
const result = await userService.createUser(userData);
expect(result).toHaveProperty('userId');
expect(result.name).toBe('John Doe');
});
Testing internal framework details
// Don't test if logging happened - that's framework responsibility
it('should log user creation', async () => {
// This tests the framework, not your business logic
});
2. Focus on Business Logicโ
it('should reject invalid email', async () => {
await expect(userService.createUser({ email: 'invalid' }))
.rejects.toThrow('Invalid email format');
});
3. Use Descriptive Test Namesโ
it('should handle non-existent user gracefully', async () => {
const result = await userService.getUserById('non-existent');
expect(result).toBeNull();
});
๐ Jest-Specific Featuresโ
1. Powerful Matchersโ
it('should use Jest matchers', async () => {
const result = await userService.createUser(userData);
expect(result).toHaveProperty('name', 'John Doe');
expect(result.email).toMatch(/@/); // Regex matcher
expect(typeof result.name).toBe('string'); // Type checking
});
2. Async Testingโ
it('should handle async operations', async () => {
// Jest handles async/await naturally
await expect(userService.getUserById('user-123'))
.resolves.not.toThrow();
});
3. Structure Validation (Better than Snapshots)โ
it('should validate object structure without snapshots', async () => {
const result = await userService.createUser(userData);
// Test structure without depending on random values
// This is better than snapshots for objects with random IDs
expect(result).toHaveProperty('userId');
expect(result).toHaveProperty('name');
expect(result).toHaveProperty('email');
expect(typeof result.userId).toBe('string');
expect(result.userId.length).toBeGreaterThan(0);
expect(result.name).toBe('John Doe');
expect(result.email).toBe('john@example.com');
});
๐ What's Being Testedโ
- โ What We Test
- โ What We Don't Test
- Business Logic: User creation, validation, retrieval
- Error Handling: Invalid inputs, edge cases
- Data Structures: Return values, object properties
- Async Operations: Promise resolution, error rejection
- Framework Features: Logging, context management, Redis operations
- External Dependencies: Database connections, HTTP calls
- Implementation Details: Internal method calls, private properties
๐จ Common Pitfallsโ
1. Testing Framework Instead of Business Logicโ
- โ Don't do this
- โ Do this instead
it('should log user creation', async () => {
// Testing if logging happened - framework responsibility
});
it('should create user successfully', async () => {
const result = await userService.createUser(userData);
expect(result).toHaveProperty('userId');
});
2. Testing External Dependenciesโ
- โ Don't do this
- โ Do this instead
it('should connect to Redis', async () => {
// Testing Redis connection - external dependency
});
it('should handle user data correctly', async () => {
const result = await userService.createUser(userData);
expect(result).toHaveProperty('userId');
});
3. Over-Complicated Setupโ
- โ Don't do this
- โ Do this instead
beforeEach(async () => {
// Complex setup with real framework initialization
await syntropyLog.initialize();
// ... more setup
});
beforeEach(() => {
testHelper.beforeEach(); // Simple, clean setup
userService = new UserService(testHelper.mockSyntropyLog);
});
๐ Next Stepsโ
- Run the tests:
npm test
- Explore the code: Look at
src/index.ts
to understand the service - Modify tests: Try adding your own test cases
- Check coverage:
npm run test:coverage
- Try other examples: Check examples 28 (Vitest) and 30 (Redis context)
๐ค Contributingโ
When adding new tests:
- Follow the Arrange-Act-Assert pattern
- Use descriptive test names
- Focus on behavior, not implementation
- Keep tests simple and readable
- Use the provided helpers for consistent setup
๐ Related Examplesโ
Coming soon: More testing examples will be added here as we create them.
The goal is to write tests that are readable, maintainable, and focused on business value. Let the framework handle the complexity, and focus your tests on what matters most: your business logic.
This example works seamlessly with the published syntropylog
package. When you install it via npm, the syntropylog/testing
module will be available automatically.