Writing Tests
This guide covers patterns and conventions for writing tests in the Ever Works platform, with examples from the actual codebase.
Testing NestJS Services
Most agent package services are tested using NestJS's Test.createTestingModule. The pattern is:
- Create mock implementations of dependencies.
- Build a testing module with the service under test and mock providers.
- Get the service instance from the compiled module.
- Assert behavior.
Example: Testing PluginRegistryService
import { Test, TestingModule } from '@nestjs/testing';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { PluginRegistryService } from '../services/plugin-registry.service';
import { WorkPluginRepository } from '../repositories/work-plugin.repository';
import { UserPluginRepository } from '../repositories/user-plugin.repository';
describe('PluginRegistryService', () => {
let service: PluginRegistryService;
let eventEmitter: EventEmitter2;
let workPluginRepository: jest.Mocked<WorkPluginRepository>;
beforeEach(async () => {
workPluginRepository = {
findByWorkAndPlugin: jest.fn()
} as unknown as jest.Mocked<WorkPluginRepository>;
const module: TestingModule = await Test.createTestingModule({
providers: [
PluginRegistryService,
{
provide: EventEmitter2,
useValue: { emit: jest.fn(), on: jest.fn(), off: jest.fn() }
},
{
provide: WorkPluginRepository,
useValue: workPluginRepository
},
{
provide: UserPluginRepository,
useValue: { findByUserAndPlugin: jest.fn() }
}
]
}).compile();
service = module.get<PluginRegistryService>(PluginRegistryService);
eventEmitter = module.get<EventEmitter2>(EventEmitter2);
});
afterEach(() => {
service.clear();
});
it('should register a plugin', () => {
const plugin = createMockPlugin('test-plugin');
service.register(plugin, createMockManifest('test-plugin'));
expect(service.get('test-plugin')).toBe(plugin);
});
});
Key Patterns
jest.Mocked<T>types mock objects so TypeScript understands mocked methods.as unknown as jest.Mocked<T>casts partial mocks (only the methods your tests need).afterEachcleanup ensures test isolation by resetting service state.Test.createTestingModulewires up the NestJS DI container for the service under test.
Testing Pure Functions
Functions without NestJS dependencies are tested directly without the testing module. This is faster and simpler.
Example: Testing Pair Selection
import { selectNextPair, buildPairKey, countRemainingPairs } from '../pair-selector';
import type { ItemData } from '@ever-works/contracts';
function makeItem(slug: string, category: string): ItemData {
return {
name: slug.charAt(0).toUpperCase() + slug.slice(1),
description: `Description of ${slug}`,
source_url: `https://${slug}.example.com`,
category,
slug,
tags: []
};
}
describe('buildPairKey', () => {
it('should produce consistent order-independent keys', () => {
expect(buildPairKey('vercel', 'netlify')).toBe('netlify--vercel');
expect(buildPairKey('netlify', 'vercel')).toBe('netlify--vercel');
});
});
describe('selectNextPair', () => {
const items = [makeItem('vercel', 'hosting'), makeItem('netlify', 'hosting'), makeItem('cloudflare', 'hosting')];
it('should return the first available pair when none generated', () => {
const result = selectNextPair({
items,
generatedPairs: [],
minItemsForComparison: 3,
maxComparisons: 50
});
expect(result).not.toBeNull();
expect(result!.itemA.slug).toBeDefined();
});
it('should skip already-generated pairs', () => {
const first = selectNextPair({
items,
generatedPairs: ['cloudflare--vercel'],
minItemsForComparison: 3,
maxComparisons: 50
});
const key = buildPairKey(first!.itemA.slug, first!.itemB.slug);
expect(key).not.toBe('cloudflare--vercel');
});
});
Creating Mock Helpers
Define reusable factory functions for test entities:
const createMockPlugin = (id: string, category = 'utility'): IPlugin =>
({
id,
name: `Plugin ${id}`,
version: '1.0.0',
category,
capabilities: ['test-capability'],
settingsSchema: { type: 'object', properties: {} },
configurationMode: 'hybrid',
onLoad: jest.fn(),
onUnload: jest.fn()
}) as unknown as IPlugin;
const createMockManifest = (id: string, category = 'utility'): PluginManifest => ({
id,
name: `Plugin ${id}`,
version: '1.0.0',
description: 'Test plugin',
category,
capabilities: ['test-capability']
});
Place these at the top of the test file or in a shared __tests__/helpers/ work if used across multiple test suites.
Mocking Facades and Repositories
The agent package uses the facade pattern to abstract external services. Tests mock at the facade boundary:
// Mock GitFacadeService
const mockGitFacade = {
cloneOrPull: jest.fn().mockResolvedValue('/tmp/repo'),
add: jest.fn().mockResolvedValue(undefined),
commit: jest.fn().mockResolvedValue(undefined),
push: jest.fn().mockResolvedValue(undefined)
};
// Mock AiFacadeService
const mockAiFacade = {
askJson: jest.fn().mockResolvedValue({
result: { items: [] },
usage: { totalTokens: 100 },
cost: 0.001
}),
createChatCompletion: jest.fn().mockResolvedValue({
choices: [{ message: { content: 'AI response' } }]
})
};
// Mock Repository
const mockWorkRepository = {
findById: jest.fn().mockResolvedValue(mockWork),
update: jest.fn().mockResolvedValue(undefined),
increment: jest.fn().mockResolvedValue(undefined)
};
Test File Location
Test files are co-located with their source code in __tests__/ works:
packages/agent/src/
plugins/
services/
plugin-registry.service.ts
__tests__/
plugin-registry.service.spec.ts
plugin-settings.service.spec.ts
comparison-generator/
comparison/
pair-selector.ts
__tests__/
pair-selector.spec.ts
facades/
git.facade.ts
__tests__/
git.facade.spec.ts
Testing Plugin Packages (Vitest)
Plugin tests use Vitest with the globals: true option, so describe, it, and expect are available without imports:
// packages/plugins/openai/src/openai.spec.ts
describe('OpenAI Plugin', () => {
it('should have correct metadata', () => {
expect(plugin.id).toBe('openai');
expect(plugin.category).toBe('ai-provider');
});
});
Run a single plugin's tests:
cd packages/plugins/openai
npx vitest run src/openai.spec.ts
Common Gotchas
- Import resolution errors -- If tests fail with module-not-found errors, ensure workspace packages are built:
pnpm buildfrom root. - Async service initialization -- Some services implement
OnModuleInit. Usemodule.init()in tests if you need lifecycle hooks to run. - Mocking TypeORM repositories -- Only mock the repository methods your test actually calls. Use
as unknown as jest.Mocked<T>for partial mocks. - ESM
.jsimports -- The JestmoduleNameMapperhandles.jsto.tsresolution. If a new pattern fails, check the mapper config.