Skip to main content

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โ€‹

npm install

๐Ÿ“‹ 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โ€‹

// 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');

Why Use the Mock?โ€‹

  • โœ… No Initialization: No need to call syntropyLog.init() or syntropyLog.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โ€‹

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');
});

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โ€‹

  • Business Logic: User creation, validation, retrieval
  • Error Handling: Invalid inputs, edge cases
  • Data Structures: Return values, object properties
  • Async Operations: Promise resolution, error rejection

๐Ÿšจ Common Pitfallsโ€‹

1. Testing Framework Instead of Business Logicโ€‹

it('should log user creation', async () => {
// Testing if logging happened - framework responsibility
});

2. Testing External Dependenciesโ€‹

it('should connect to Redis', async () => {
// Testing Redis connection - external dependency
});

3. Over-Complicated Setupโ€‹

beforeEach(async () => {
// Complex setup with real framework initialization
await syntropyLog.initialize();
// ... more setup
});

๐Ÿ“š Next Stepsโ€‹

  1. Run the tests: npm test
  2. Explore the code: Look at src/index.ts to understand the service
  3. Modify tests: Try adding your own test cases
  4. Check coverage: npm run test:coverage
  5. Try other examples: Check examples 28 (Vitest) and 30 (Redis context)

๐Ÿค Contributingโ€‹

When adding new tests:

  1. Follow the Arrange-Act-Assert pattern
  2. Use descriptive test names
  3. Focus on behavior, not implementation
  4. Keep tests simple and readable
  5. Use the provided helpers for consistent setup

Coming soon: More testing examples will be added here as we create them.


Remember

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.

Framework Integration

This example works seamlessly with the published syntropylog package. When you install it via npm, the syntropylog/testing module will be available automatically.