Repository navigation
Backend Testing
github-actions[bot] edited this page Oct 13, 2026
·
2 revisions
Canonical source:
docs/09-testing/backend-testing-guide.md— this page is rendered from it on every publish; edit the source.
Vitest patterns, mocking Prisma, mocking RabbitMQ, test file structure, and examples.
Each backend service has its own vitest.config.ts. Tests use:
-
Extension:
*.spec.ts -
Location:
__tests__/directories adjacent to source -
Runner: Vitest (
vitest run) with theunplugin-swctransform (emits decorator metadata Nest DI needs; esbuild does not).globals: true, sodescribe/it/expect/vineed no import -
Module aliases: Path aliases matching
tsconfig.json
apps/claw-auth-service/
src/
modules/auth/
managers/
auth.manager.ts
__tests__/
auth.manager.spec.ts
services/
auth.service.ts
__tests__/
auth.service.spec.ts
dto/
login.dto.ts
__tests__/
login.dto.spec.ts
common/utilities/
hashing.utility.ts
__tests__/
hashing.utility.spec.ts
__tests__/
app.spec.ts # Bootstrap test
Use describe/it blocks with behavior-focused descriptions:
describe('ChatThreadsService', () => {
let service: ChatThreadsService;
let mockPrisma: MockPrismaService;
let mockRabbitMQ: MockRabbitMQService;
beforeEach(() => {
mockPrisma = createMockPrisma();
mockRabbitMQ = createMockRabbitMQ();
service = new ChatThreadsService(mockPrisma, mockRabbitMQ);
});
describe('create', () => {
it('should create a thread with valid input', async () => {
mockPrisma.chatThread.create.mockResolvedValue(mockThread);
const result = await service.create(validInput, userId);
expect(result.title).toBe(validInput.title);
expect(mockPrisma.chatThread.create).toHaveBeenCalledWith(
expect.objectContaining({ data: expect.objectContaining({ userId }) }),
);
});
it('should set default routing mode when none provided', async () => {
mockPrisma.chatThread.create.mockResolvedValue(mockThread);
const result = await service.create({ title: 'Test' }, userId);
expect(result.routingMode).toBe(RoutingMode.AUTO);
});
});
describe('findByUser', () => {
it('should return only threads owned by the user', async () => {
mockPrisma.chatThread.findMany.mockResolvedValue([mockThread]);
const result = await service.findByUser(userId, {});
expect(mockPrisma.chatThread.findMany).toHaveBeenCalledWith(
expect.objectContaining({ where: expect.objectContaining({ userId }) }),
);
});
it('should paginate results', async () => {
mockPrisma.chatThread.findMany.mockResolvedValue([]);
mockPrisma.chatThread.count.mockResolvedValue(0);
const result = await service.findByUser(userId, { page: 2, limit: 10 });
expect(mockPrisma.chatThread.findMany).toHaveBeenCalledWith(
expect.objectContaining({ skip: 10, take: 10 }),
);
});
});
});Create a mock that mirrors the Prisma client structure:
function createMockPrisma() {
return {
chatThread: {
create: vi.fn(),
findUnique: vi.fn(),
findFirst: vi.fn(),
findMany: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
count: vi.fn(),
},
chatMessage: {
create: vi.fn(),
findMany: vi.fn(),
findUnique: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
count: vi.fn(),
},
// Add other models as needed
$transaction: vi.fn((fn) => fn(createMockPrisma())),
};
}const module = await Test.createTestingModule({
providers: [
ChatThreadsService,
{
provide: PrismaService,
useValue: createMockPrisma(),
},
{
provide: RabbitMQService,
useValue: createMockRabbitMQ(),
},
],
}).compile();
service = module.get(ChatThreadsService);// Return a single entity
mockPrisma.chatThread.findUnique.mockResolvedValue(mockThread);
// Return null (not found)
mockPrisma.chatThread.findUnique.mockResolvedValue(null);
// Return a list
mockPrisma.chatThread.findMany.mockResolvedValue([mockThread1, mockThread2]);
// Throw an error
mockPrisma.chatThread.create.mockRejectedValue(new Error('Unique constraint violation'));
// Transaction
mockPrisma.$transaction.mockImplementation(async (fn) => fn(mockPrisma));function createMockRabbitMQ() {
return {
publish: vi.fn().mockResolvedValue(undefined),
subscribe: vi.fn(),
};
}it('should publish thread.created event', async () => {
mockPrisma.chatThread.create.mockResolvedValue(mockThread);
await service.create(validInput, userId);
expect(mockRabbitMQ.publish).toHaveBeenCalledWith(
'thread.created',
expect.objectContaining({
threadId: mockThread.id,
userId,
}),
);
});const mockHttpService = {
get: vi.fn(),
post: vi.fn(),
};
// Mock a successful response
mockHttpService.get.mockReturnValue({
pipe: vi.fn().mockReturnValue(of({ data: mockResponse })),
});const mockCacheManager = {
get: vi.fn(),
set: vi.fn(),
del: vi.fn(),
};const mockLogger = {
log: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
};describe('CreateConnectorDto', () => {
const validData = {
name: 'My OpenAI Connector',
provider: 'OPENAI',
config: { apiKey: 'sk-test-key' },
};
it('should accept valid connector data', () => {
const result = createConnectorSchema.safeParse(validData);
expect(result.success).toBe(true);
});
it('should reject missing name', () => {
const { name, ...noName } = validData;
const result = createConnectorSchema.safeParse(noName);
expect(result.success).toBe(false);
});
it('should reject name exceeding max length', () => {
const result = createConnectorSchema.safeParse({
...validData,
name: 'a'.repeat(256),
});
expect(result.success).toBe(false);
});
it('should reject invalid provider', () => {
const result = createConnectorSchema.safeParse({
...validData,
provider: 'INVALID',
});
expect(result.success).toBe(false);
});
});it('should create a memory record', async () => {
const input = { content: 'User prefers dark mode', type: MemoryType.PREFERENCE };
mockPrisma.memoryRecord.create.mockResolvedValue({ id: '1', ...input, userId });
const result = await service.create(input, userId);
expect(result.content).toBe(input.content);
expect(result.type).toBe(MemoryType.PREFERENCE);
});it('should throw EntityNotFoundException when thread not found', async () => {
mockPrisma.chatThread.findUnique.mockResolvedValue(null);
await expect(service.findOne('nonexistent-id', userId)).rejects.toThrow(EntityNotFoundException);
});it('should throw ForbiddenException when user does not own the thread', async () => {
mockPrisma.chatThread.findUnique.mockResolvedValue({
...mockThread,
userId: 'other-user',
});
await expect(service.delete('thread-id', 'my-user-id')).rejects.toThrow(BusinessException);
});describe('RoutingManager', () => {
describe('routeMessage', () => {
it('should call Ollama for AUTO mode routing', async () => {
mockOllamaClient.generate.mockResolvedValue({
provider: 'anthropic',
model: 'claude-sonnet-4',
});
const result = await manager.routeMessage(message, RoutingMode.AUTO);
expect(mockOllamaClient.generate).toHaveBeenCalled();
expect(result.selectedProvider).toBe('anthropic');
});
it('should fall back to heuristic routing on Ollama failure', async () => {
mockOllamaClient.generate.mockRejectedValue(new Error('Ollama unavailable'));
const result = await manager.routeMessage(message, RoutingMode.AUTO);
expect(result.selectedProvider).toBeDefined();
expect(result.confidence).toBeLessThan(100);
});
});
});Every service has an app.spec.ts that verifies dependency injection:
describe('App Module', () => {
it('should create the application module', async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();
const app = module.createNestApplication();
await app.init();
expect(app).toBeDefined();
await app.close();
});
});This catches:
- Missing provider registrations
- Circular dependency injection errors
- Invalid module configuration
# All backend tests
npm run test
# Specific service
npm run test --workspace=apps/claw-auth-service
# Watch mode
cd apps/claw-auth-service && npx vitest
# Single file
cd apps/claw-chat-service && npx vitest run src/modules/chat/services/__tests__/chat.service.spec.ts
# With coverage
cd apps/claw-auth-service && npx vitest run --coverage
# Run matching test names
cd apps/claw-auth-service && npx vitest run -t "should create"ClawAI Wiki · source-controlled under wiki/ · generated/mirrored from the current repository knowledge layer.
ClawAI Wiki
Product
- Product-Vision
- Flagship-Features
- Business-Overview
- Requirements-Register
- Drift-Log
- Runtime-Managed-Connector-Providers
- Threads
- ADR-Index
Architecture
- System-at-a-Glance
- Architecture-Map
- System-Architecture
- Message-Flow
- End-to-End-Data-Flow
- Routing-Engine
- Event-Bus
- Security-Architecture
Platform
- Backend-Services
- Frontend-Architecture
- Shared-Packages
- Database-Reference
- API-Reference
- Provider-Catalog
- Memory-and-Context-Architecture
- File-Support-Architecture
- Research-and-Web-Crawling
Runtime & Ops
- Docker-Guide
- Nginx-Reference
- CI-CD-Pipeline
- Logging-and-Observability
- Operational-Runbooks
- Troubleshooting
Quality
Engineering OS
- AI-Native-Engineering-OS
- Generated-Manifests
- Rules-Catalog
- Skills-Catalog
- Context-Layer
- Engineering-Memory
- Tooling-and-Automation
- Scripts-and-Operations
- ESLint-and-Architecture-Enforcement
- Extended-Work-Skills
Agents
Reference