Skip to main content

Testing Philosophy

ZapDev uses a mock-first testing approach with centralized mocks for external dependencies. All tests run in a Node environment (not DOM) using Jest and ts-jest.

Test Setup

Running Tests

# Run all tests
bun run test

# Run tests in watch mode
bun run test --watch

# Run tests with coverage
bun run test --coverage

# Run specific test file
bun run test tests/credit-system.test.ts

Test Configuration

Our Jest configuration is defined in jest.config.js:
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/tests'],
  testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
  transform: {
    '^.+\.ts$': 'ts-jest',
  },
  moduleNameMapper: {
    '^@/convex/_generated/api$': '<rootDir>/tests/mocks/convex-generated-api.ts',
    '^@/convex/_generated/dataModel$': '<rootDir>/tests/mocks/convex-generated-dataModel.ts',
    '^@/convex/(.*)$': '<rootDir>/convex/$1',
    '^@/(.*)$': '<rootDir>/src/$1',
    '^@inngest/agent-kit$': '<rootDir>/tests/mocks/inngest-agent-kit.ts',
    '^@e2b/code-interpreter$': '<rootDir>/tests/mocks/e2b-code-interpreter.ts',
    '^convex/browser$': '<rootDir>/tests/mocks/convex-browser.ts',
  },
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.d.ts',
    '!src/generated/**',
  ],
  moduleFileExtensions: ['ts', 'js', 'json'],
  setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
};

Test Structure

Test Organization

Tests are located in the /tests directory with the following structure:
tests/
├── mocks/                           # Centralized mocks
│   ├── convex-browser.ts
│   ├── convex-generated-api.ts
│   ├── convex-generated-dataModel.ts
│   ├── e2b-code-interpreter.ts
│   └── inngest-agent-kit.ts
├── agent-improvements.test.ts       # AI agent enhancement tests
├── agent-workflow.test.ts           # Agent workflow tests
├── auth-helpers.test.ts             # Authentication tests
├── credit-system.test.ts            # Credit/usage tracking tests
├── file-operations.test.ts          # File handling tests
├── frameworks.test.ts               # Framework selection tests
├── gateway-fallback.test.ts         # API gateway failover tests
├── glm-subagent-system.test.ts      # GLM subagent tests
├── model-selection.test.ts          # Model selection tests
├── sanitizers.test.ts               # Input validation tests
├── security.test.ts                 # Security tests
├── setup.ts                         # Test setup configuration
└── utils.test.ts                    # Utility function tests

Test File Naming

  • Unit tests: *.test.ts
  • Integration tests: *.integration.test.ts
  • Test utilities: *.spec.ts

Writing Tests

Basic Test Structure

import { describe, it, expect, beforeEach } from '@jest/globals';

describe('Feature Name', () => {
  beforeEach(() => {
    // Setup before each test
  });

  describe('Specific Functionality', () => {
    it('should do something specific', () => {
      // Arrange
      const input = 'test';
      
      // Act
      const result = someFunction(input);
      
      // Assert
      expect(result).toBe('expected');
    });
  });
});

Using Mocks

All external dependencies are mocked in tests/mocks/:
// E2B sandbox operations are automatically mocked
import { Sandbox } from '@e2b/code-interpreter';

// Convex queries/mutations are mocked
import { api } from '@/convex/_generated/api';

// Inngest workflows are mocked
import { inngest } from '@inngest/agent-kit';

Example: Testing Credit System

import { describe, it, expect, beforeEach } from '@jest/globals';

const CREDIT_LIMITS = {
  free: 5,
  pro: 100
};

describe('Credit System', () => {
  let creditSystem: CreditSystem;

  beforeEach(() => {
    creditSystem = new CreditSystem();
  });

  describe('Free Tier Credits', () => {
    it('should give 5 credits to free tier users', async () => {
      await creditSystem.createUsageRecord('user_1', 'free', 0);
      
      const usage = await creditSystem.getUsageForUser('user_1');
      
      expect(usage.creditsRemaining).toBe(5);
      expect(usage.creditsLimit).toBe(5);
      expect(usage.planType).toBe('free');
    });

    it('should throw error when free credits exhausted', async () => {
      await creditSystem.createUsageRecord('user_3', 'free', 5);
      
      await expect(
        creditSystem.checkAndConsumeCreditForUser('user_3')
      ).rejects.toThrow('You have run out of credits');
    });
  });
});

Testing Best Practices

DO’s

Test one thing per test case
Use descriptive test names that explain the expected behavior
Follow the Arrange-Act-Assert pattern
Mock external dependencies using centralized mocks
Test edge cases and error conditions
Keep tests isolated and independent

DON’Ts

Don’t test implementation details
Don’t skip tests or use .only in committed code
Don’t use real API calls or database connections
Don’t write tests that depend on execution order
Don’t use any type in test code

Testing Different Layers

Testing Utilities

Test pure functions with simple input/output:
import { sanitizePath } from '@/lib/sanitizers';

describe('sanitizePath', () => {
  it('should remove leading slashes', () => {
    expect(sanitizePath('/path/to/file')).toBe('path/to/file');
  });

  it('should handle relative paths', () => {
    expect(sanitizePath('../../../etc/passwd')).toBe('etc/passwd');
  });
});

Testing tRPC Procedures

Test tRPC routers with mocked context:
import { appRouter } from '@/trpc/routers/_app';

const mockContext = {
  userId: 'user_123',
  session: { /* mock session */ }
};

const caller = appRouter.createCaller(mockContext);

it('should fetch project by id', async () => {
  const project = await caller.projects.getById({ id: 'proj_1' });
  expect(project.id).toBe('proj_1');
});

Testing AI Agents

Test agent orchestration logic with mocked E2B sandboxes:
import { generateCode } from '@/agents/code-agent';

it('should generate Next.js application', async () => {
  const result = await generateCode({
    prompt: 'Create a todo app',
    framework: 'nextjs'
  });
  
  expect(result.files).toContain('app/page.tsx');
  expect(result.success).toBe(true);
});

Testing Convex Functions

Test Convex queries and mutations:
import { getUserUsage } from '@/convex/usage';

it('should return usage for authenticated user', async () => {
  const usage = await getUserUsage({ userId: 'user_1' });
  
  expect(usage.creditsRemaining).toBeGreaterThanOrEqual(0);
  expect(usage.planType).toMatch(/^(free|pro)$/);
});

Coverage Goals

  • Overall Coverage: Aim for 80%+ code coverage
  • Critical Paths: 100% coverage for:
    • Authentication logic
    • Credit/usage tracking
    • Security validations
    • File sanitization
  • Nice to Have: High coverage for:
    • UI components (70%+)
    • Utility functions (90%+)
    • API routes (80%+)

Continuous Integration

Tests run automatically on:
  • Every pull request
  • Every commit to main branch
  • Before deployment
All CI checks must pass before a PR can be merged.

Debugging Tests

Running Tests in Debug Mode

# Use Node inspector
node --inspect-brk node_modules/.bin/jest --runInBand

# Add console.log statements
console.log('Debug value:', someValue);

# Use VSCode debugger with breakpoints

Common Issues

IssueSolution
Mock not workingCheck moduleNameMapper in jest.config.js
Type errors in testsEnsure @types/jest is installed
Timeout errorsIncrease timeout: jest.setTimeout(10000)
Path resolutionVerify @/* aliases in tsconfig

Test Maintenance

1

Keep Tests Updated

When you change code, update the corresponding tests
2

Remove Obsolete Tests

Delete tests for removed features
3

Refactor Tests

Keep tests DRY by extracting common setup to helpers
4

Monitor Coverage

Review coverage reports regularly

Resources


Well-tested code is maintainable code. Happy testing!

Build docs developers (and LLMs) love