/sdk
/typescript
/testing

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!