Testing and Quality Assurance

1. Jest TypeScript Configuration and Type Testing

Configuration Package/Setting Description Use Case
ts-jest npm i -D ts-jest @types/jest TypeScript preprocessor for Jest - compiles TS tests Jest with TypeScript support
preset preset: 'ts-jest' Pre-configured Jest setup for TypeScript Quick Jest TS configuration
testEnvironment 'node' | 'jsdom' Test execution environment - Node.js or browser Backend vs frontend tests
globals globals: { 'ts-jest': {...} } ts-jest specific configuration options TypeScript compiler tweaks
@testing-library @testing-library/react Testing utilities with full TypeScript support Component testing
Type Assertions expect(value).toBe<Type> Type-aware Jest matchers Runtime + type checking

Example: Jest configuration for TypeScript

// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
  transform: {
    "^.+\\.ts$": "ts-jest",
  },
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.d.ts',
    '!src/**/*.test.ts'
  ],
  globals: {
    'ts-jest': {
      tsconfig: {
        esModuleInterop: true,
        allowSyntheticDefaultImports: true
      }
    }
  }
};

// package.json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  },
  "devDependencies": {
    "@types/jest": "^29.5.0",
    "jest": "^29.5.0",
    "ts-jest": "^29.1.0",
    "typescript": "^5.0.0"
  }
}

Example: Writing typed tests with Jest

import { sum, fetchUser, User } from './math';

describe('sum function', () => {
  it('should add two numbers', () => {
    const result: number = sum(2, 3);
    expect(result).toBe(5);
  });
  
  it('should handle negative numbers', () => {
    expect(sum(-1, 1)).toBe(0);
  });
});

describe('fetchUser function', () => {
  it('should return a user object', async () => {
    const user: User = await fetchUser(1);
    
    expect(user).toHaveProperty('id');
    expect(user).toHaveProperty('name');
    expect(user).toHaveProperty('email');
    expect(typeof user.id).toBe('number');
    expect(typeof user.name).toBe('string');
  });
  
  it('should throw error for invalid id', async () => {
    await expect(fetchUser(-1)).rejects.toThrow('Invalid user ID');
  });
});

// Testing generics
function identity<T>(value: T): T {
  return value;
}

describe('identity function', () => {
  it('should preserve type for string', () => {
    const result: string = identity('hello');
    expect(result).toBe('hello');
  });
  
  it('should preserve type for number', () => {
    const result: number = identity(42);
    expect(result).toBe(42);
  });
  
  it('should preserve type for object', () => {
    const obj = { name: 'John', age: 30 };
    const result: typeof obj = identity(obj);
    expect(result).toEqual(obj);
  });
});

2. Type-only Tests and expect-type Library

Library/Feature Syntax Description Use Case
expect-type npm i -D expect-type Compile-time type assertions - no runtime code Type-only testing
expectTypeOf expectTypeOf(val).toEqualTypeOf<T>() Assert exact type match Verify precise types
toMatchTypeOf expectTypeOf(val).toMatchTypeOf<T>() Assert type is assignable to target Check type compatibility
tsd npm i -D tsd Test TypeScript type definitions - .d.ts files Library type definitions
dtslint npm i -D dtslint DefinitelyTyped testing tool @types packages
@ts-expect-error // @ts-expect-error Assert that next line has type error Negative type tests

Example: Type-only tests with expect-type

import { expectTypeOf } from 'expect-type';

// Test function return types
function add(a: number, b: number): number {
  return a + b;
}

expectTypeOf(add).returns.toEqualTypeOf<number>();
expectTypeOf(add).parameter(0).toEqualTypeOf<number>();
expectTypeOf(add).parameters.toEqualTypeOf<[number, number]>();

// Test generic types
function identity<T>(value: T): T {
  return value;
}

expectTypeOf(identity<string>).returns.toEqualTypeOf<string>();
expectTypeOf(identity<number>).returns.toEqualTypeOf<number>();

// Test type utilities
type User = { id: number; name: string; email: string };
type UserUpdate = Partial<User>;

expectTypeOf<UserUpdate>().toMatchTypeOf<{ id?: number }>();
expectTypeOf<UserUpdate>().toMatchTypeOf<{ name?: string }>();

// Test object types
interface Product {
  id: number;
  name: string;
  price: number;
}

const product: Product = { id: 1, name: 'Widget', price: 9.99 };

expectTypeOf(product).toEqualTypeOf<Product>();
expectTypeOf(product).toHaveProperty('id');
expectTypeOf(product.id).toEqualTypeOf<number>();
expectTypeOf(product.name).toEqualTypeOf<string>();

// Test union and intersection types
type StringOrNumber = string | number;
type NameAndAge = { name: string } & { age: number };

expectTypeOf<StringOrNumber>().toMatchTypeOf<string>();
expectTypeOf<StringOrNumber>().toMatchTypeOf<number>();
expectTypeOf<NameAndAge>().toHaveProperty('name');
expectTypeOf<NameAndAge>().toHaveProperty('age');

Example: Negative type tests

// Test that code produces type errors
function requiresNumber(n: number): void {}

// @ts-expect-error - should fail with string argument
requiresNumber('hello');

// @ts-expect-error - should fail with undefined
requiresNumber(undefined);

// This should compile fine (no error)
requiresNumber(42);

// Test type narrowing
function process(value: string | number) {
  if (typeof value === 'string') {
    // @ts-expect-error - toFixed doesn't exist on string
    value.toFixed(2);
    
    // This is fine
    value.toUpperCase();
  }
}

// Test readonly enforcement
interface ReadonlyConfig {
  readonly apiKey: string;
}

const config: ReadonlyConfig = { apiKey: 'secret' };

// @ts-expect-error - cannot assign to readonly property
config.apiKey = 'new-secret';

// Using expect-type for negative tests
import { expectTypeOf } from 'expect-type';

expectTypeOf<string>().not.toEqualTypeOf<number>();
expectTypeOf<{ a: string }>().not.toMatchTypeOf<{ b: number }>();

// Test that types are NOT assignable
type Admin = { role: 'admin'; permissions: string[] };
type User = { role: 'user'; name: string };

expectTypeOf<Admin>().not.toMatchTypeOf<User>();
expectTypeOf<User>().not.toMatchTypeOf<Admin>();

3. Mock Typing and Stub Generation

Feature Syntax Description Use Case
jest.fn() jest.fn<ReturnType, Args>() Create typed mock function Mock functions with types
jest.Mock<T> Mock<ReturnType, Args> Type for Jest mock function Type mock variables
jest.mocked() jest.mocked(fn, deep?) Type helper for mocked modules Type module mocks
Partial<T> Mocks Partial<Interface> Create partial object mocks Mock complex objects
jest.spyOn() jest.spyOn<T, K>(obj, method) Create typed spy on method Monitor method calls
ts-mockito npm i -D ts-mockito Type-safe mocking library Alternative to Jest mocks

Example: Typed mocks with Jest

import { jest } from '@jest/globals';

// Mock typed functions
type FetchUser = (id: number) => Promise<User>;

const mockFetchUser = jest.fn<Promise<User>, [number]>();

mockFetchUser.mockResolvedValue({
  id: 1,
  name: 'John',
  email: 'john@example.com'
});

// Use in tests
const user = await mockFetchUser(1);
expect(mockFetchUser).toHaveBeenCalledWith(1);
expect(user.name).toBe('John');

// Mock class methods
class UserService {
  async getUser(id: number): Promise<User> {
    // implementation
  }
  
  async updateUser(id: number, data: Partial<User>): Promise<User> {
    // implementation
  }
}

const mockUserService = {
  getUser: jest.fn<Promise<User>, [number]>(),
  updateUser: jest.fn<Promise<User>, [number, Partial<User>]>()
};

mockUserService.getUser.mockResolvedValue({
  id: 1,
  name: 'Alice',
  email: 'alice@example.com'
});

// Partial mocks for complex objects
interface ApiClient {
  baseURL: string;
  timeout: number;
  get<T>(path: string): Promise<T>;
  post<T>(path: string, data: any): Promise<T>;
}

const mockApiClient: Partial<ApiClient> = {
  get: jest.fn(),
  post: jest.fn()
};

// Type-safe spy
const userService = new UserService();
const getUserSpy = jest.spyOn(userService, 'getUser');

getUserSpy.mockResolvedValue({ id: 1, name: 'Bob', email: 'bob@example.com' });

await userService.getUser(1);
expect(getUserSpy).toHaveBeenCalledWith(1);

Example: Module mocking with types

// api.ts
export async function fetchUsers(): Promise<User[]> {
  const response = await fetch('/api/users');
  return response.json();
}

export async function createUser(data: CreateUserData): Promise<User> {
  const response = await fetch('/api/users', {
    method: 'POST',
    body: JSON.stringify(data)
  });
  return response.json();
}

// api.test.ts
import * as api from './api';

jest.mock('./api');

// Type the mocked module
const mockedApi = jest.mocked(api);

describe('User API', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });
  
  it('should fetch users', async () => {
    const mockUsers: User[] = [
      { id: 1, name: 'Alice', email: 'alice@example.com' },
      { id: 2, name: 'Bob', email: 'bob@example.com' }
    ];
    
    mockedApi.fetchUsers.mockResolvedValue(mockUsers);
    
    const users = await api.fetchUsers();
    
    expect(users).toHaveLength(2);
    expect(users[0].name).toBe('Alice');
    expect(mockedApi.fetchUsers).toHaveBeenCalledTimes(1);
  });
  
  it('should create user', async () => {
    const newUser: User = { id: 3, name: 'Charlie', email: 'charlie@example.com' };
    const createData: CreateUserData = { name: 'Charlie', email: 'charlie@example.com' };
    
    mockedApi.createUser.mockResolvedValue(newUser);
    
    const result = await api.createUser(createData);
    
    expect(result).toEqual(newUser);
    expect(mockedApi.createUser).toHaveBeenCalledWith(createData);
  });
});

// Using ts-mockito for advanced mocking
import { mock, instance, when, verify, anything } from 'ts-mockito';

class UserRepository {
  async findById(id: number): Promise<User | null> {
    // implementation
  }
}

describe('UserRepository with ts-mockito', () => {
  it('should find user by id', async () => {
    const mockRepo = mock(UserRepository);
    const user: User = { id: 1, name: 'Alice', email: 'alice@example.com' };
    
    when(mockRepo.findById(1)).thenResolve(user);
    
    const repo = instance(mockRepo);
    const result = await repo.findById(1);
    
    expect(result).toEqual(user);
    verify(mockRepo.findById(1)).once();
  });
});

4. Test Coverage and Type Coverage Analysis

Tool Command/Config Description Metric
Jest Coverage jest --coverage Measure code coverage - lines, branches, functions Runtime test coverage
type-coverage npx type-coverage Calculate TypeScript type coverage percentage Type annotation coverage
coverageThreshold coverageThreshold: { global: {...} } Enforce minimum coverage requirements CI/CD quality gates
istanbul nyc typescript Code coverage tool used by Jest Coverage reports
strict: true tsconfig.json Enable all strict type-checking options Type safety enforcement
noImplicitAny "noImplicitAny": true Require explicit types, disallow implicit any Explicit typing

Example: Jest coverage configuration

// jest.config.js
module.exports = {
  preset: 'ts-jest',
  collectCoverage: true,
  coverageDirectory: 'coverage',
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/*.test.{ts,tsx}',
    '!src/index.ts'
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  },
  coverageReporters: ['text', 'lcov', 'html', 'json'],
  coveragePathIgnorePatterns: [
    '/node_modules/',
    '/dist/',
    '/__tests__/',
    '/coverage/'
  ]
};

// package.json scripts
{
  "scripts": {
    "test": "jest",
    "test:coverage": "jest --coverage",
    "test:coverage:watch": "jest --coverage --watchAll",
    "coverage:report": "open coverage/lcov-report/index.html"
  }
}

Example: Type coverage analysis

// Install type-coverage
npm install -D type-coverage

// package.json
{
  "scripts": {
    "type-coverage": "type-coverage",
    "type-coverage:detail": "type-coverage --detail",
    "type-coverage:strict": "type-coverage --strict"
  }
}

// Run type coverage
npx type-coverage

// Output example:
// 2345 / 2456 95.48%
// type-coverage success: 95.48% >= 95.00%

// Detailed report shows files with low coverage
npx type-coverage --detail

// Output:
// path/to/file.ts:45:12 error implicit any
// path/to/file.ts:78:5 error implicit any
// ...

// Configuration in package.json
{
  "typeCoverage": {
    "atLeast": 95,
    "strict": true,
    "ignoreCatch": true,
    "ignoreFiles": [
      "**/*.test.ts",
      "**/*.spec.ts",
      "**/test/**"
    ]
  }
}

// tsconfig.json for strict type checking
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}

5. Property-Based Testing with TypeScript

Library Feature Description Use Case
fast-check npm i -D fast-check Property-based testing library for TypeScript Generate test cases automatically
fc.property() fc.property(arb, predicate) Define property test with arbitraries Test properties hold for all inputs
Arbitraries fc.integer(), fc.string() Value generators for different types Random test data generation
fc.assert() fc.assert(property) Run property test with generated values Execute property tests
Shrinking Automatic minimal failing case Find smallest input that breaks property Easier debugging
Custom Arbitraries fc.record(), fc.tuple() Create complex type generators Test custom types

Example: Property-based testing with fast-check

import fc from 'fast-check';

// Test that addition is commutative
describe('Addition properties', () => {
  it('should be commutative', () => {
    fc.assert(
      fc.property(fc.integer(), fc.integer(), (a, b) => {
        return a + b === b + a;
      })
    );
  });
  
  it('should be associative', () => {
    fc.assert(
      fc.property(fc.integer(), fc.integer(), fc.integer(), (a, b, c) => {
        return (a + b) + c === a + (b + c);
      })
    );
  });
  
  it('should have identity element (0)', () => {
    fc.assert(
      fc.property(fc.integer(), (a) => {
        return a + 0 === a && 0 + a === a;
      })
    );
  });
});

// Test string reverse function
function reverse(str: string): string {
  return str.split('').reverse().join('');
}

describe('String reverse properties', () => {
  it('reversing twice returns original', () => {
    fc.assert(
      fc.property(fc.string(), (str) => {
        return reverse(reverse(str)) === str;
      })
    );
  });
  
  it('reverse preserves length', () => {
    fc.assert(
      fc.property(fc.string(), (str) => {
        return reverse(str).length === str.length;
      })
    );
  });
});

// Test array sorting
describe('Array sort properties', () => {
  it('sorted array has same length', () => {
    fc.assert(
      fc.property(fc.array(fc.integer()), (arr) => {
        const sorted = [...arr].sort((a, b) => a - b);
        return sorted.length === arr.length;
      })
    );
  });
  
  it('sorted array is ordered', () => {
    fc.assert(
      fc.property(fc.array(fc.integer()), (arr) => {
        const sorted = [...arr].sort((a, b) => a - b);
        return sorted.every((val, i, arr) =>
          i === 0 || arr[i - 1] <= val
        );
      })
    );
  });
});

Example: Custom arbitraries for complex types

import fc from 'fast-check';

// Define User type
interface User {
  id: number;
  name: string;
  email: string;
  age: number;
  isActive: boolean;
}

// Custom arbitrary for User
const userArbitrary = fc.record({
  id: fc.integer({ min: 1 }),
  name: fc.string({ minLength: 1, maxLength: 50 }),
  email: fc.emailAddress(),
  age: fc.integer({ min: 18, max: 100 }),
  isActive: fc.boolean()
});

// Test user validation function
function isValidUser(user: User): boolean {
  return (
    user.id > 0 &&
    user.name.length > 0 &&
    user.email.includes('@') &&
    user.age >= 18 &&
    user.age <= 100
  );
}

describe('User validation', () => {
  it('should validate generated users', () => {
    fc.assert(
      fc.property(userArbitrary, (user) => {
        return isValidUser(user);
      })
    );
  });
});

// Complex nested structures
interface Address {
  street: string;
  city: string;
  zipCode: string;
}

interface Company {
  name: string;
  address: Address;
  employees: User[];
}

const addressArbitrary = fc.record({
  street: fc.string(),
  city: fc.string(),
  zipCode: fc.string({ minLength: 5, maxLength: 5 })
});

const companyArbitrary = fc.record({
  name: fc.string(),
  address: addressArbitrary,
  employees: fc.array(userArbitrary, { minLength: 1, maxLength: 100 })
});

// Test company operations
describe('Company operations', () => {
  it('should not lose employees when reorganizing', () => {
    fc.assert(
      fc.property(companyArbitrary, (company) => {
        const originalCount = company.employees.length;
        const reorganized = reorganizeCompany(company);
        return reorganized.employees.length === originalCount;
      })
    );
  });
});

6. Contract Testing and API Type Validation

Tool/Library Purpose Description Use Case
Zod npm i zod TypeScript-first schema validation - runtime + types API validation, forms
io-ts npm i io-ts Runtime type checking with static types API boundaries
Yup npm i yup Schema builder with TypeScript support Form validation
Pact @pact-foundation/pact Consumer-driven contract testing Microservices contracts
OpenAPI/Swagger openapi-typescript Generate TypeScript from OpenAPI specs API type generation
MSW npm i -D msw Mock Service Worker with TypeScript API mocking, testing

Example: Zod schema validation

import { z } from 'zod';

// Define schema
const UserSchema = z.object({
  id: z.number().positive(),
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().min(18).max(120),
  role: z.enum(['admin', 'user', 'guest']),
  createdAt: z.date(),
  tags: z.array(z.string()).optional()
});

// Infer TypeScript type from schema
type User = z.infer<typeof UserSchema>;
// Result:
// type User = {
//   id: number;
//   name: string;
//   email: string;
//   age: number;
//   role: "admin" | "user" | "guest";
//   createdAt: Date;
//   tags?: string[] | undefined;
// }

// Validate data
function createUser(data: unknown): User {
  // Throws if validation fails
  return UserSchema.parse(data);
}

// Safe validation
function createUserSafe(data: unknown): { success: true; data: User } | { success: false; error: z.ZodError } {
  const result = UserSchema.safeParse(data);
  return result;
}

// API endpoint validation
app.post('/users', async (req, res) => {
  const result = UserSchema.safeParse(req.body);
  
  if (!result.success) {
    return res.status(400).json({
      error: 'Validation failed',
      details: result.error.issues
    });
  }
  
  const user = result.data; // Typed as User
  await saveUser(user);
  res.status(201).json(user);
});

// Nested schemas
const AddressSchema = z.object({
  street: z.string(),
  city: z.string(),
  zipCode: z.string().regex(/^\d{5}$/)
});

const CompanySchema = z.object({
  name: z.string(),
  address: AddressSchema,
  employees: z.array(UserSchema),
  revenue: z.number().nonnegative()
});

type Company = z.infer<typeof CompanySchema>;

Example: Contract testing with OpenAPI

// Generate TypeScript types from OpenAPI spec
// npm install -D openapi-typescript
// npx openapi-typescript ./api-spec.yaml -o ./api-types.ts

// api-types.ts (generated)
export interface paths {
  '/users': {
    get: {
      responses: {
        200: {
          content: {
            'application/json': User[];
          };
        };
      };
    };
    post: {
      requestBody: {
        content: {
          'application/json': CreateUserRequest;
        };
      };
      responses: {
        201: {
          content: {
            'application/json': User;
          };
        };
      };
    };
  };
  '/users/{id}': {
    get: {
      parameters: {
        path: {
          id: number;
        };
      };
      responses: {
        200: {
          content: {
            'application/json': User;
          };
        };
      };
    };
  };
}

// Use generated types
import type { paths } from './api-types';

type GetUsersResponse = paths['/users']['get']['responses'][200]['content']['application/json'];
type CreateUserRequest = paths['/users']['post']['requestBody']['content']['application/json'];
type GetUserParams = paths['/users/{id}']['get']['parameters']['path'];

// Type-safe API client
class ApiClient {
  async getUsers(): Promise<GetUsersResponse> {
    const response = await fetch('/users');
    return response.json();
  }
  
  async createUser(data: CreateUserRequest): Promise<User> {
    const response = await fetch('/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
    return response.json();
  }
  
  async getUser(params: GetUserParams): Promise<User> {
    const response = await fetch(`/users/${params.id}`);
    return response.json();
  }
}

Example: MSW for API mocking

import { rest } from 'msw';
import { setupServer } from 'msw/node';

// Define mock handlers with types
const server = setupServer(
  rest.get<never, { id: string }, User>('/users/:id', (req, res, ctx) => {
    const { id } = req.params;
    
    return res(
      ctx.status(200),
      ctx.json({
        id: Number(id),
        name: 'John Doe',
        email: 'john@example.com',
        age: 30,
        role: 'user',
        createdAt: new Date()
      })
    );
  }),
  
  rest.post<CreateUserRequest, never, User>('/users', async (req, res, ctx) => {
    const body = await req.json();
    
    return res(
      ctx.status(201),
      ctx.json({
        id: 123,
        ...body,
        createdAt: new Date()
      })
    );
  }),
  
  rest.get<never, never, User[]>('/users', (req, res, ctx) => {
    const page = req.url.searchParams.get('page') || '1';
    
    return res(
      ctx.status(200),
      ctx.json([
        { id: 1, name: 'Alice', email: 'alice@example.com', age: 25, role: 'user', createdAt: new Date() },
        { id: 2, name: 'Bob', email: 'bob@example.com', age: 30, role: 'admin', createdAt: new Date() }
      ])
    );
  })
);

// Setup for tests
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

// Use in tests
describe('API integration', () => {
  it('should fetch user by id', async () => {
    const user = await apiClient.getUser({ id: 1 });
    
    expect(user.id).toBe(1);
    expect(user.name).toBe('John Doe');
  });
  
  it('should handle errors', async () => {
    server.use(
      rest.get('/users/:id', (req, res, ctx) => {
        return res(ctx.status(404), ctx.json({ error: 'Not found' }));
      })
    );
    
    await expect(apiClient.getUser({ id: 999 })).rejects.toThrow();
  });
});
Note: Testing best practices:
  • Jest config - Use ts-jest preset, configure coverage thresholds, separate test configs
  • Type-only tests - Use expect-type or @ts-expect-error for compile-time assertions
  • Mocks - Type all mocks with generics, use jest.mocked() for module mocks
  • Coverage - Track both code coverage (Jest) and type coverage (type-coverage tool)
  • Property testing - Use fast-check for automated test case generation
  • Validation - Use Zod or io-ts for runtime validation with TypeScript types

Testing and Quality Assurance Summary

  • Jest - Configure with ts-jest preset, type all mocks and tests for full type safety
  • Type testing - Use expect-type for compile-time type assertions, @ts-expect-error for negative tests
  • Mocking - Type mocks with jest.fn<ReturnType, Args>, use Partial<T> for complex objects
  • Coverage - Measure code coverage with Jest, type coverage with type-coverage, enforce thresholds
  • Property testing - Use fast-check for generative testing with custom arbitraries
  • Validation - Integrate Zod/io-ts for runtime validation, OpenAPI for contract testing