Testing Guide
Nexgent uses Jest for testing. This guide covers testing patterns and best practices.
Test Structure
packages/backend/tests/
├── helpers/ # Test utilities
│ ├── test-db.ts # Database helpers
│ ├── test-factory.ts # Mock data factories
│ └── test-redis.ts # Redis helpers
├── unit/ # Unit tests
│ └── domain/
│ ├── agents/
│ ├── trading/
│ └── signals/
└── integration/ # Integration tests
├── cache/
├── repositories/
└── services/Running Tests
All Tests
# Run all tests
pnpm test
# Run with coverage
pnpm test -- --coverageBackend Tests
# All backend tests
pnpm --filter backend test
# Unit tests only
pnpm --filter backend test:unit
# Integration tests only
pnpm --filter backend test:integration
# Specific test file
pnpm --filter backend test -- agent-service.test.ts
# Watch mode
pnpm --filter backend test -- --watchUnit Tests
Unit tests test individual functions/classes in isolation with mocked dependencies.
Structure
// tests/unit/domain/agents/agent-service.test.ts
import { AgentService } from '@/domain/agents/agent-service.js';
// Mock dependencies
jest.mock('@/infrastructure/cache/redis-config-service.js', () => ({
redisConfigService: {
setAgentConfig: jest.fn(),
invalidateAgentConfig: jest.fn(),
},
}));
describe('AgentService', () => {
let agentService: AgentService;
let mockRepository: jest.Mocked<IAgentRepository>;
beforeEach(() => {
// Reset mocks before each test
jest.clearAllMocks();
// Create mock repository
mockRepository = {
create: jest.fn(),
findById: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
};
// Create service with mocked dependencies
agentService = new AgentService(mockRepository);
});
describe('create', () => {
it('should create agent with default config', async () => {
// Arrange
const input = {
name: 'Test Agent',
userId: 'user-123',
tradingMode: 'simulation' as const,
};
mockRepository.create.mockResolvedValue({
id: 'agent-123',
...input,
});
// Act
const result = await agentService.create(input);
// Assert
expect(result.name).toBe('Test Agent');
expect(mockRepository.create).toHaveBeenCalledWith(
expect.objectContaining({ name: 'Test Agent' })
);
});
it('should throw error for invalid input', async () => {
// Arrange
const input = { name: '', userId: 'user-123' };
// Act & Assert
await expect(agentService.create(input))
.rejects
.toThrow('Name is required');
});
});
});Mocking Patterns
// Mock module
jest.mock('@/infrastructure/database/client.js', () => ({
prisma: {
agent: {
findUnique: jest.fn(),
create: jest.fn(),
},
},
}));
// Mock function
const mockFn = jest.fn();
mockFn.mockReturnValue('value');
mockFn.mockResolvedValue(asyncValue);
mockFn.mockRejectedValue(new Error('error'));
// Mock implementation
mockFn.mockImplementation((arg) => arg * 2);
// Verify calls
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith(expectedArg);
expect(mockFn).toHaveBeenCalledTimes(2);Integration Tests
Integration tests test multiple components together with real or test databases.
Database Setup
// tests/helpers/test-db.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export async function setupTestDatabase() {
// Clean tables before each test
await prisma.agentTransaction.deleteMany();
await prisma.agentPosition.deleteMany();
await prisma.agent.deleteMany();
await prisma.user.deleteMany();
}
export async function teardownTestDatabase() {
await prisma.$disconnect();
}
export { prisma };Redis Setup
// tests/helpers/test-redis.ts
import Redis from 'ioredis';
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
db: 1, // Use separate DB for tests
});
export async function flushTestRedis() {
await redis.flushdb();
}
export { redis };Integration Test Example
// tests/integration/services/agent-service.integration.test.ts
import { setupTestDatabase, teardownTestDatabase, prisma } from '../../helpers/test-db';
import { flushTestRedis } from '../../helpers/test-redis';
import { AgentService } from '@/domain/agents/agent-service.js';
describe('AgentService Integration', () => {
let agentService: AgentService;
let testUserId: string;
beforeAll(async () => {
await setupTestDatabase();
// Create test user
const user = await prisma.user.create({
data: { email: 'test@example.com', passwordHash: 'hash' },
});
testUserId = user.id;
});
afterAll(async () => {
await teardownTestDatabase();
});
beforeEach(async () => {
await flushTestRedis();
await prisma.agent.deleteMany({ where: { userId: testUserId } });
agentService = new AgentService();
});
it('should create and retrieve agent', async () => {
// Create
const agent = await agentService.create({
name: 'Integration Test Agent',
userId: testUserId,
tradingMode: 'simulation',
});
expect(agent.id).toBeDefined();
// Retrieve
const retrieved = await agentService.getById(agent.id);
expect(retrieved?.name).toBe('Integration Test Agent');
});
it('should cache agent config in Redis', async () => {
const agent = await agentService.create({
name: 'Cache Test Agent',
userId: testUserId,
tradingMode: 'simulation',
});
// Config should be cached
const cachedConfig = await redis.get(`agent:${agent.id}:config`);
expect(cachedConfig).toBeDefined();
});
});Test Factories
Use factories to create consistent test data:
// tests/helpers/test-factory.ts
import { randomUUID } from 'crypto';
import type { Agent, TradingConfig } from 'nexgent-open-source-trading-engine/shared';
export function createMockAgentId(): string {
return randomUUID();
}
export function createMockAgent(overrides?: Partial<Agent>): Agent {
return {
id: createMockAgentId(),
userId: createMockAgentId(),
name: 'Test Agent',
tradingMode: 'simulation',
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
};
}
export function createMockConfig(overrides?: Partial<TradingConfig>): TradingConfig {
return {
purchaseLimits: {
minimumAgentBalance: 0.5,
maxPurchasePerToken: 2.0,
},
stopLoss: {
enabled: true,
defaultPercentage: -32,
mode: 'fixed',
trailingLevels: [],
},
// ... other defaults
...overrides,
};
}Testing Patterns
Arrange-Act-Assert
it('should calculate stop loss correctly', () => {
// Arrange
const purchasePrice = 100;
const currentPrice = 150;
const config = createMockConfig({ stopLoss: { percentage: -20 } });
// Act
const stopLoss = calculateStopLoss(purchasePrice, currentPrice, config);
// Assert
expect(stopLoss).toBe(120); // 150 * 0.8
});Testing Errors
it('should throw error for invalid agent', async () => {
await expect(
agentService.getById('invalid-id')
).rejects.toThrow('Agent not found');
});
it('should throw specific error type', async () => {
await expect(
agentService.create({ name: '' })
).rejects.toThrow(AgentServiceError);
});Testing Async Code
it('should process signals in parallel', async () => {
const signals = [signal1, signal2, signal3];
const results = await Promise.all(
signals.map(s => processor.process(s))
);
expect(results).toHaveLength(3);
expect(results.every(r => r.success)).toBe(true);
});Coverage
Running Coverage
pnpm --filter backend test -- --coverageCoverage Targets
| Metric | Target |
|---|---|
| Statements | > 80% |
| Branches | > 75% |
| Functions | > 80% |
| Lines | > 80% |
What to Test
Always test:
- Business logic (domain services)
- Validation logic
- Error handling
- Edge cases
Usually test:
- Repository methods
- API handlers
- Utility functions
Skip testing:
- Third-party library wrappers
- Simple getters/setters
- Generated code (Prisma client)
Best Practices
1. Test Behavior, Not Implementation
// Good: Test the behavior
it('should return active agents only', async () => {
const agents = await service.getActiveAgents();
expect(agents.every(a => a.isActive)).toBe(true);
});
// Avoid: Testing implementation details
it('should call findMany with isActive filter', async () => {
await service.getActiveAgents();
expect(prisma.agent.findMany).toHaveBeenCalledWith({
where: { isActive: true },
});
}); // ❌ Brittle - breaks if implementation changes2. One Assertion Per Test
// Good: Focused tests
it('should set correct name', () => {
expect(agent.name).toBe('Test Agent');
});
it('should set correct trading mode', () => {
expect(agent.tradingMode).toBe('simulation');
});
// Avoid: Multiple unrelated assertions
it('should create agent correctly', () => {
expect(agent.name).toBe('Test Agent');
expect(agent.tradingMode).toBe('simulation');
expect(agent.isActive).toBe(true);
expect(agent.balance).toBe(0);
}); // ❌ If one fails, unclear which3. Use Descriptive Test Names
// Good: Describes scenario and expectation
it('should throw error when balance is below minimum', () => {});
it('should skip agent when already has position in token', () => {});
// Avoid: Vague names
it('should work', () => {}); // ❌
it('test agent', () => {}); // ❌