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