Skip to main content

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:

  1. Create mock implementations of dependencies.
  2. Build a testing module with the service under test and mock providers.
  3. Get the service instance from the compiled module.
  4. 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).
  • afterEach cleanup ensures test isolation by resetting service state.
  • Test.createTestingModule wires 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

  1. Import resolution errors -- If tests fail with module-not-found errors, ensure workspace packages are built: pnpm build from root.
  2. Async service initialization -- Some services implement OnModuleInit. Use module.init() in tests if you need lifecycle hooks to run.
  3. Mocking TypeORM repositories -- Only mock the repository methods your test actually calls. Use as unknown as jest.Mocked<T> for partial mocks.
  4. ESM .js imports -- The Jest moduleNameMapper handles .js to .ts resolution. If a new pattern fails, check the mapper config.