Testing Guide
Overview
NitroStack v3.0 provides testing utilities to help you write unit and integration tests for your MCP servers.
Setup
Install Dependencies
npm install --save-dev jest @types/jest ts-jest
Jest Configuration
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
}
};
Package.json Script
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
Testing Module
Import Testing Utilities
import {
TestingModule,
createMockContext,
createMockFn,
spyOn
} from 'nitrostack/testing';
TestingModule
Create a testing module to test your components in isolation:
import { TestingModule } from 'nitrostack/testing';
import { ProductsModule } from '../products/products.module';
import { ProductService } from '../products/products.service';
describe('ProductsModule', () => {
let module: TestingModule;
let productService: ProductService;
beforeEach(async () => {
module = await TestingModule.create({
imports: [ProductsModule],
providers: [
{
provide: DatabaseService,
useValue: mockDatabaseService // Mock dependency
}
]
});
productService = module.get(ProductService);
});
afterEach(async () => {
await module.close();
});
it('should be defined', () => {
expect(productService).toBeDefined();
});
});
Testing Tools
Basic Tool Test
import { createMockContext } from 'nitrostack/testing';
import { ProductsTools } from '../products/products.tools';
import { ProductService } from '../products/products.service';
describe('ProductsTools', () => {
let tools: ProductsTools;
let mockProductService: jest.Mocked<ProductService>;
beforeEach(() => {
mockProductService = {
findById: jest.fn(),
search: jest.fn()
} as any;
tools = new ProductsTools(mockProductService);
});
describe('getProduct', () => {
it('should return product by ID', async () => {
const mockProduct = {
id: 'prod-1',
name: 'Test Product',
price: 99.99
};
mockProductService.findById.mockResolvedValue(mockProduct);
const ctx = createMockContext();
const result = await tools.getProduct({ product_id: 'prod-1' }, ctx);
expect(result).toEqual(mockProduct);
expect(mockProductService.findById).toHaveBeenCalledWith('prod-1');
});
it('should throw if product not found', async () => {
mockProductService.findById.mockResolvedValue(null);
const ctx = createMockContext();
await expect(
tools.getProduct({ product_id: 'invalid' }, ctx)
).rejects.toThrow('Product not found');
});
});
});
Testing with Authentication
describe('AuthenticatedTools', () => {
it('should use authenticated user ID', async () => {
const ctx = createMockContext({
auth: {
subject: 'user-123',
token: 'fake-token'
}
});
const result = await tools.getUserProfile({}, ctx);
expect(result.id).toBe('user-123');
});
});
Testing Guards
import { JWTGuard } from '../guards/jwt.guard';
describe('JWTGuard', () => {
let guard: JWTGuard;
let mockConfigService: any;
beforeEach(() => {
mockConfigService = {
get: jest.fn().mockReturnValue('test-secret')
};
guard = new JWTGuard(mockConfigService);
});
it('should allow valid token', async () => {
const ctx = createMockContext({
metadata: {
authorization: 'Bearer valid-token'
}
});
// Mock JWT verification
jest.spyOn(jwt, 'verify').mockReturnValue({
sub: 'user-123',
email: 'test@example.com'
});
const result = await guard.canActivate(ctx);
expect(result).toBe(true);
expect(ctx.auth?.subject).toBe('user-123');
});
it('should reject missing token', async () => {
const ctx = createMockContext();
const result = await guard.canActivate(ctx);
expect(result).toBe(false);
});
it('should reject invalid token', async () => {
const ctx = createMockContext({
metadata: {
authorization: 'Bearer invalid-token'
}
});
jest.spyOn(jwt, 'verify').mockImplementation(() => {
throw new Error('Invalid token');
});
const result = await guard.canActivate(ctx);
expect(result).toBe(false);
});
});
Testing Services
Basic Service Test
describe('ProductService', () => {
let service: ProductService;
let mockDb: jest.Mocked<DatabaseService>;
beforeEach(() => {
mockDb = {
query: jest.fn(),
queryOne: jest.fn(),
execute: jest.fn()
} as any;
service = new ProductService(mockDb);
});
describe('findById', () => {
it('should return product', async () => {
const mockProduct = { id: 'prod-1', name: 'Test' };
mockDb.queryOne.mockResolvedValue(mockProduct);
const result = await service.findById('prod-1');
expect(result).toEqual(mockProduct);
expect(mockDb.queryOne).toHaveBeenCalledWith(
'SELECT * FROM products WHERE id = ?',
['prod-1']
);
});
});
describe('search', () => {
it('should search products', async () => {
const mockProducts = [
{ id: 'prod-1', name: 'Test 1' },
{ id: 'prod-2', name: 'Test 2' }
];
mockDb.query.mockResolvedValue(mockProducts);
const result = await service.search('test');
expect(result).toEqual(mockProducts);
expect(mockDb.query).toHaveBeenCalledWith(
expect.stringContaining('LIKE'),
['%test%']
);
});
});
});
Testing Middleware
import { LoggingMiddleware } from '../middleware/logging.middleware';
describe('LoggingMiddleware', () => {
let middleware: LoggingMiddleware;
let ctx: any;
let next: jest.Mock;
beforeEach(() => {
middleware = new LoggingMiddleware();
ctx = createMockContext();
next = jest.fn().mockResolvedValue('result');
});
it('should log before and after execution', async () => {
const logSpy = jest.spyOn(ctx.logger, 'info');
const result = await middleware.use(ctx, next);
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Before'));
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('After'));
expect(next).toHaveBeenCalled();
expect(result).toBe('result');
});
it('should log errors', async () => {
const error = new Error('Test error');
next.mockRejectedValue(error);
const errorSpy = jest.spyOn(ctx.logger, 'error');
await expect(middleware.use(ctx, next)).rejects.toThrow(error);
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Error'));
});
});
Testing Interceptors
import { TransformInterceptor } from '../interceptors/transform.interceptor';
describe('TransformInterceptor', () => {
let interceptor: TransformInterceptor;
let ctx: any;
let next: jest.Mock;
beforeEach(() => {
interceptor = new TransformInterceptor();
ctx = createMockContext();
next = jest.fn();
});
it('should wrap result in success response', async () => {
next.mockResolvedValue({ id: 1, name: 'Test' });
const result = await interceptor.intercept(ctx, next);
expect(result).toEqual({
success: true,
data: { id: 1, name: 'Test' },
metadata: expect.any(Object)
});
});
it('should include timestamp', async () => {
next.mockResolvedValue({ data: 'test' });
const result = await interceptor.intercept(ctx, next);
expect(result.metadata.timestamp).toBeDefined();
});
});
Integration Tests
Testing Full Module
describe('ProductsModule Integration', () => {
let module: TestingModule;
let tools: ProductsTools;
let db: DatabaseService;
beforeEach(async () => {
// Use real database (in-memory SQLite)
const testDb = new DatabaseService(':memory:');
await testDb.migrate();
module = await TestingModule.create({
imports: [ProductsModule],
providers: [
{ provide: DatabaseService, useValue: testDb }
]
});
tools = module.get(ProductsTools);
db = module.get(DatabaseService);
// Seed test data
await db.execute(
'INSERT INTO products (id, name, price) VALUES (?, ?, ?)',
['prod-1', 'Test Product', 99.99]
);
});
afterEach(async () => {
await module.close();
});
it('should fetch product from database', async () => {
const ctx = createMockContext();
const result = await tools.getProduct({ product_id: 'prod-1' }, ctx);
expect(result).toMatchObject({
id: 'prod-1',
name: 'Test Product',
price: 99.99
});
});
it('should search products', async () => {
const ctx = createMockContext();
const result = await tools.searchProducts({ query: 'Test' }, ctx);
expect(result.products).toHaveLength(1);
expect(result.products[0].name).toBe('Test Product');
});
});
Mock Utilities
createMockContext
const ctx = createMockContext({
auth: {
subject: 'user-123',
token: 'fake-token',
role: 'admin'
},
metadata: {
custom: 'value'
}
});
createMockFn
const mockFn = createMockFn<(input: any) => Promise<any>>();
mockFn.mockResolvedValue({ success: true });
mockFn.mockRejectedValue(new Error('Failed'));
spyOn
const spy = spyOn(service, 'methodName');
spy.mockReturnValue('mocked value');
spy.mockImplementation((arg) => `modified ${arg}`);
expect(spy).toHaveBeenCalledWith('arg');
expect(spy).toHaveBeenCalledTimes(1);
Test Coverage
Run Coverage
npm run test:coverage
Coverage Goals
Aim for:
- Statements: > 80%
- Branches: > 70%
- Functions: > 80%
- Lines: > 80%
Jest Configuration
// jest.config.js
module.exports = {
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.spec.ts',
'!src/**/*.test.ts',
'!src/**/index.ts'
],
coverageThresholds: {
global: {
branches: 70,
functions: 80,
lines: 80,
statements: 80
}
}
};
Best Practices
1. Test Business Logic
// ✅ Good - Test logic
it('should calculate discount correctly', () => {
const result = service.calculateDiscount(100, 0.2);
expect(result).toBe(80);
});
// ❌ Avoid - Testing implementation details
it('should call calculatePrice', () => {
service.calculateDiscount(100, 0.2);
expect(mockCalculatePrice).toHaveBeenCalled();
});
2. Use Descriptive Test Names
// ✅ Good
it('should throw error when product ID is invalid', () => {});
it('should return null when user is not found', () => {});
// ❌ Avoid
it('test1', () => {});
it('should work', () => {});
3. Arrange-Act-Assert Pattern
// ✅ Good
it('should update user name', async () => {
// Arrange
const userId = 'user-1';
const newName = 'John Doe';
// Act
const result = await service.updateUser(userId, { name: newName });
// Assert
expect(result.name).toBe(newName);
});
4. Mock External Dependencies
// ✅ Good - Mock external API
jest.mock('axios');
axios.get.mockResolvedValue({ data: { weather: 'sunny' } });
// ❌ Avoid - Real API calls in tests
const weather = await axios.get('https://api.weather.com');
5. Test Edge Cases
describe('calculateTotal', () => {
it('should handle empty cart', () => {
expect(service.calculateTotal([])).toBe(0);
});
it('should handle negative quantities', () => {
expect(() => service.calculateTotal([{ qty: -1 }]))
.toThrow('Invalid quantity');
});
it('should handle very large numbers', () => {
const result = service.calculateTotal([
{ price: Number.MAX_SAFE_INTEGER, qty: 1 }
]);
expect(result).toBe(Number.MAX_SAFE_INTEGER);
});
});
Example Test Suite
// products.service.spec.ts
import { ProductService } from './products.service';
import { DatabaseService } from '../database/database.service';
describe('ProductService', () => {
let service: ProductService;
let mockDb: jest.Mocked<DatabaseService>;
beforeEach(() => {
mockDb = {
query: jest.fn(),
queryOne: jest.fn(),
execute: jest.fn()
} as any;
service = new ProductService(mockDb);
});
describe('findById', () => {
it('should return product when found', async () => {
const mockProduct = { id: 'prod-1', name: 'Test', price: 99.99 };
mockDb.queryOne.mockResolvedValue(mockProduct);
const result = await service.findById('prod-1');
expect(result).toEqual(mockProduct);
});
it('should return null when not found', async () => {
mockDb.queryOne.mockResolvedValue(null);
const result = await service.findById('invalid');
expect(result).toBeNull();
});
it('should throw on database error', async () => {
mockDb.queryOne.mockRejectedValue(new Error('DB Error'));
await expect(service.findById('prod-1')).rejects.toThrow('DB Error');
});
});
describe('search', () => {
it('should return matching products', async () => {
const mockProducts = [
{ id: 'prod-1', name: 'Test 1' },
{ id: 'prod-2', name: 'Test 2' }
];
mockDb.query.mockResolvedValue(mockProducts);
const result = await service.search('test');
expect(result).toHaveLength(2);
expect(mockDb.query).toHaveBeenCalledWith(
expect.any(String),
['%test%']
);
});
it('should return empty array when no matches', async () => {
mockDb.query.mockResolvedValue([]);
const result = await service.search('nonexistent');
expect(result).toEqual([]);
});
});
describe('create', () => {
it('should create new product', async () => {
mockDb.execute.mockResolvedValue({ lastInsertRowid: 1 });
const input = { name: 'New Product', price: 49.99 };
const result = await service.create(input);
expect(result.id).toBeDefined();
expect(result.name).toBe('New Product');
expect(mockDb.execute).toHaveBeenCalledWith(
expect.stringContaining('INSERT'),
expect.arrayContaining([input.name, input.price])
);
});
});
});
Next Steps
Pro Tip: Write tests as you develop, not after. Test-driven development (TDD) leads to better design and fewer bugs!