Skip to content

TypeScript Testing — Vitest, Jest, and Cypress Guide

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about TypeScript Testing. We cover key concepts, practical examples, and best practices to help you master this topic.

TypeScript testing with Vitest or Jest catches logic errors and type mismatches simultaneously — your tests verify runtime behavior while TypeScript ensures mock data matches real interfaces.

What You'll Learn

  • Setting up Vitest with TypeScript
  • Writing unit tests for typed functions
  • Mocking typed dependencies
  • Integration testing with databases
  • E2E testing with Cypress
  • Test-Driven Development (TDD)

Why It Matters

Untyped tests let you mock incorrect shapes — a mock that returns { name: "Alice" } when the real function expects { fullName: "Alice" } passes tests but fails in production. TypeScript makes tests as type-safe as production code.

Real-World Use

The Doda Browser sync service has 2,000+ TypeScript tests. Every API endpoint, database query, and sync algorithm is tested with typed mocks that mirror the production interfaces. TypeScript catches mock mismatches before tests even run.

Learning Path

flowchart LR
  A[Database Access] --> B[Testing]
  B --> C[Advanced Patterns]
  B --> D[You Are Here]
  C --> E[SOLID Principles]
  D --> F[Error Handling]

Setting Up Vitest

Vitest is the modern TypeScript testing framework with native ESM and TypeScript support:

npm install --save-dev vitest
// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: ['src/**/*.test.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
    },
  },
});

Add to package.json:

{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest"
  }
}

Writing Type-Safe Unit Tests

// src/utils/format.ts
export function formatUserName(firstName: string, lastName: string): string {
  return `${firstName} ${lastName}`.trim();
}

export function calculateDiscount(price: number, discountPercent: number): number {
  if (discountPercent < 0 || discountPercent > 100) {
    throw new Error('Discount must be between 0 and 100');
  }
  return price * (1 - discountPercent / 100);
}

// src/utils/format.test.ts
import { describe, it, expect } from 'vitest';
import { formatUserName, calculateDiscount } from './format';

describe('formatUserName', () => {
  it('combines first and last name with a space', () => {
    const result = formatUserName('Alice', 'Johnson');
    expect(result).toBe('Alice Johnson');
  });

  it('handles empty strings', () => {
    const result = formatUserName('', 'Smith');
    expect(result).toBe('Smith');
  });

  it('trims whitespace', () => {
    const result = formatUserName('  Bob  ', '  Jones  ');
    expect(result).toBe('Bob Jones');
  });
});

describe('calculateDiscount', () => {
  it('applies percentage discount correctly', () => {
    expect(calculateDiscount(100, 20)).toBe(80);
  });

  it('throws for invalid discount percentage', () => {
    expect(() => calculateDiscount(100, -1)).toThrow();
    expect(() => calculateDiscount(100, 101)).toThrow();
  });

  it('returns 0 for 100% discount', () => {
    expect(calculateDiscount(100, 100)).toBe(0);
  });
});

Expected output:

✓ formatUserName
  ✓ combines first and last name with a space
  ✓ handles empty strings
  ✓ trims whitespace
✓ calculateDiscount
  ✓ applies percentage discount correctly
  ✓ throws for invalid discount percentage
  ✓ returns 0 for 100% discount

Mocking Typed Dependencies

Vitest's vi.mock works seamlessly with TypeScript:

// src/services/user-service.ts
import { prisma } from '../lib/db';

export async function getUserEmail(userId: string): Promise<string | null> {
  const user = await prisma.user.findUnique({
    where: { id: userId },
    select: { email: true },
  });
  return user?.email ?? null;
}

// src/services/user-service.test.ts
import { describe, it, expect, vi } from 'vitest';
import { getUserEmail } from './user-service';

// Mock the entire db module
vi.mock('../lib/db', () => ({
  prisma: {
    user: {
      findUnique: vi.fn(),
    },
  },
}));

// Import the mocked module (now with typed mocks)
import { prisma } from '../lib/db';

const mockFindUnique = vi.mocked(prisma.user.findUnique);

describe('getUserEmail', () => {
  it('returns the email when user exists', async () => {
    // Mock the return value with the correct type
    mockFindUnique.mockResolvedValue({ email: 'alice@example.com' });

    const email = await getUserEmail('user-1');
    expect(email).toBe('alice@example.com');
    expect(mockFindUnique).toHaveBeenCalledWith({
      where: { id: 'user-1' },
      select: { email: true },
    });
  });

  it('returns null when user does not exist', async () => {
    mockFindUnique.mockResolvedValue(null);

    const email = await getUserEmail('non-existent');
    expect(email).toBeNull();
  });
});

The vi.mocked() utility wraps the mock function with correct TypeScript types. Without it, mockFindUnique.mockResolvedValue would accept any value.

Integration Testing with Supertest

Test Express routes with typed requests:

npm install --save-dev supertest @types/supertest
// src/app.test.ts
import { describe, it, expect } from 'vitest';
import request from 'supertest';
import app from './app'; // Express app

describe('GET /health', () => {
  it('returns status ok', async () => {
    const response = await request(app)
      .get('/health')
      .expect('Content-Type', /json/)
      .expect(200);

    expect(response.body).toEqual({
      status: 'ok',
      timestamp: expect.any(String),
    });
  });
});

describe('POST /users', () => {
  it('creates a user with valid data', async () => {
    const response = await request(app)
      .post('/users')
      .send({ name: 'Alice', email: 'alice@example.com' })
      .expect(201);

    expect(response.body).toHaveProperty('id');
    expect(response.body.name).toBe('Alice');
  });

  it('returns 400 for invalid email', async () => {
    await request(app)
      .post('/users')
      .send({ name: 'Alice', email: 'not-an-email' })
      .expect(400);
  });
});

Testing Asynchronous Code

TypeScript's async/await is a natural fit for test assertions:

// src/services/payment-service.ts
export interface PaymentResult {
  success: boolean;
  transactionId?: string;
  error?: string;
}

export async function processPayment(amount: number, cardNumber: string): Promise<PaymentResult> {
  if (amount <= 0) {
    return { success: false, error: 'Amount must be positive' };
  }
  if (cardNumber.length < 13) {
    return { success: false, error: 'Invalid card number' };
  }

  // Simulate payment processing
  await new Promise((r) => setTimeout(r, 10));
  return { success: true, transactionId: `txn_${Date.now()}` };
}

// src/services/payment-service.test.ts
describe('processPayment', () => {
  it('succeeds with valid amount and card', async () => {
    const result = await processPayment(50, '4111111111111111');
    expect(result.success).toBe(true);
    expect(result.transactionId).toMatch(/^txn_/);
  });

  it('fails with negative amount', async () => {
    const result = await processPayment(-10, '4111111111111111');
    expect(result.success).toBe(false);
    expect(result.error).toBe('Amount must be positive');
  });

  it('fails with short card number', async () => {
    const result = await processPayment(50, '123');
    expect(result.success).toBe(false);
    expect(result.error).toBe('Invalid card number');
  });
});

Test Coverage Configuration

npx vitest run --coverage

Configure coverage thresholds in vitest.config.ts:

coverage: {
  provider: 'v8',
  reporter: ['text', 'json', 'html'],
  thresholds: {
    branches: 80,
    functions: 80,
    lines: 80,
    statements: 80,
  },
}

E2E Testing with Cypress

Cypress supports TypeScript natively:

// cypress/e2e/login.cy.ts
describe('Login Flow', () => {
  beforeEach(() => {
    cy.visit('/login');
  });

  it('displays validation errors for empty fields', () => {
    cy.get('[data-testid="login-button"]').click();
    cy.contains('Email is required').should('be.visible');
    cy.contains('Password is required').should('be.visible');
  });

  it('logs in with valid credentials', () => {
    cy.get('[data-testid="email-input"]').type('user@example.com');
    cy.get('[data-testid="password-input"]').type('validPassword123');
    cy.get('[data-testid="login-button"]').click();
    cy.url().should('include', '/dashboard');
    cy.contains('Welcome back').should('be.visible');
  });
});

Common Mistakes

1. Testing implementation details instead of behavior

Test what the function does, not how it does it. Avoid asserting on internal state or private methods.

2. Mocking too many things

Over-mocking makes tests brittle. Mock only external boundaries (database, network, file system).

3. Not testing error paths

Happy-path tests miss 50% of the code. Always test error conditions, edge cases, and boundary values.

4. Using real databases in unit tests

Integration tests can use test databases, but unit tests should mock database access. It's slower and unreliable otherwise.

5. Forgetting to clear mocks between tests

Mock state carries over. Use vi.clearAllMocks() in afterEach or beforeEach.

6. Not using vi.mocked() for better type inference

Without vi.mocked(), mock function types default to any, losing type safety.

7. Writing tests that depend on execution order

Tests should be isolated and runnable in any order. Use beforeEach for setup, never rely on previous test state.

Practice Questions

  1. What's the difference between Vitest and Jest? Vitest is faster (uses Vite/Esbuild), has native TypeScript support, and matches Jest's API. Jest requires additional configuration for TypeScript.

  2. How do you mock a named export in Vitest? Use vi.mock('./module', () => ({ namedExport: vi.fn() })) or vi.spyOn(module, 'namedExport').

  3. What's the purpose of vi.mocked()? It wraps a mocked function with correct TypeScript types so TypeScript knows the return type and parameter types of the mock.

  4. How do you test functions that throw errors? Wrap in expect(() => fn()).toThrow() for synchronous, or await expect(fn()).rejects.toThrow() for async.

  5. What's the difference between unit and integration tests? Unit tests test single functions/classes in isolation (mocked dependencies). Integration tests test multiple components together (real database, real API calls).

Challenge

Write a test suite for a shopping cart service: test adding items, removing items, applying discounts, checking out, and error cases. Use typed mocks for the payment service and database.

FAQ

Should I use Vitest or Jest?

Vitest is the modern choice — faster, TypeScript-native, and compatible with Jest's API. Jest is mature but requires ts-jest or Babel for TypeScript.

How do I test TypeScript enums?

Enums are plain values at runtime. Test them like constants: expect(Direction.Left).toBe('left') or test functions that use enums with different values.

Can I test private methods?

Test public behavior, not private implementation. If a private method is complex enough to unit-test, extract it to its own module and test it publicly.

How do I set up Vitest in a monorepo?

Vitest supports monorepos with workspace configuration. Define a root vitest.workspace.ts that references individual package configs.

What's code coverage and should I enforce it?

Coverage measures what percentage of code is executed during tests. Aim for 80%+ but focus on critical paths, not arbitrary thresholds.

How do I test React components with TypeScript and Vitest?

Use @testing-library/react with Vitest. Component tests assert on rendered output and behavior, using vi.fn() for typed event handlers.

Mini Project

Write a comprehensive test suite for a task management API:

  • Unit tests: Task service (create, update, delete, complete)
  • Integration tests: Express routes with supertest
  • Mocked database: Mock Prisma client with typed mocks
  • Error tests: Validation failures, not found, unauthorized
  • Coverage: Configure 80% thresholds

What's Next

Your TypeScript testing skills ensure production reliability. Now explore advanced patterns with {{< ref "49-advanced-patterns" >}}, or learn SOLID principles with {{< ref "50-solid-principles" >}}.

For error handling patterns, see {{< ref "51-error-handling" >}}.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro