OS Trading Engine
Contributing
Testing Guide

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 -- --coverage

Backend 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 -- --watch

Unit 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 -- --coverage

Coverage Targets

MetricTarget
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 changes

2. 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 which

3. 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', () => {});   // ❌