TypeScript Testing Frameworks
Testing is an essential part of software development that helps ensure your code works as expected and remains maintainable as it evolves. TypeScript's static typing brings additional benefits to testing by catching type-related bugs before your tests even run. In this guide, we'll explore the most popular testing frameworks for TypeScript applications and how to use them effectively.
Introduction to Testing in TypeScript
Testing TypeScript code brings the advantages of type safety to your test suites. When you write tests in TypeScript, you get:
- Type checking in test files
- Better IDE support with autocompletion
- Improved refactoring capabilities
- Documentation through types
Let's dive into the most popular testing frameworks that work well with TypeScript.
Jest
Jest is one of the most popular JavaScript testing frameworks, and it has excellent TypeScript support.
Setting Up Jest with TypeScript
To get started with Jest and TypeScript, you'll need to install a few dependencies:
npm install --save-dev jest @types/jest ts-jest
Create a jest.config.js
file in your project root:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],
};
Writing Your First Jest Test
Let's create a simple function to test:
// src/math.ts
export function add(a: number, b: number): number {
return a + b;
}
export function subtract(a: number, b: number): number {
return a - b;
}
Now, let's write a test for this function:
// src/__tests__/math.test.ts
import { add, subtract } from '../math';
describe('Math functions', () => {
test('add function correctly adds two numbers', () => {
expect(add(2, 3)).toBe(5);
expect(add(-1, 1)).toBe(0);
expect(add(0, 0)).toBe(0);
});
test('subtract function correctly subtracts two numbers', () => {
expect(subtract(5, 2)).toBe(3);
expect(subtract(1, 1)).toBe(0);
expect(subtract(0, 5)).toBe(-5);
});
});
Run the tests with:
npx jest
Output:
PASS src/__tests__/math.test.ts
Math functions
✓ add function correctly adds two numbers (3 ms)
✓ subtract function correctly subtracts two numbers (1 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.5 s
Ran all test suites.
Jest with React and TypeScript
For testing React components written in TypeScript, you'll need to add React Testing Library:
npm install --save-dev @testing-library/react @testing-library/jest-dom
Example of testing a React component:
// src/components/Greeting.tsx
import React from 'react';
interface GreetingProps {
name: string;
}
export const Greeting: React.FC<GreetingProps> = ({ name }) => {
return <h1>Hello, {name}!</h1>;
};
// src/components/__tests__/Greeting.test.tsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { Greeting } from '../Greeting';
describe('Greeting Component', () => {
test('renders greeting message with the provided name', () => {
render(<Greeting name="John" />);
expect(screen.getByRole('heading')).toHaveTextContent('Hello, John!');
});
});
Mocha & Chai
Mocha is a flexible testing framework that can be paired with Chai (an assertion library) for TypeScript testing.
Setting Up Mocha with TypeScript
Install the necessary packages:
npm install --save-dev mocha chai @types/mocha @types/chai ts-node
Create a .mocharc.json
configuration file:
{
"require": "ts-node/register",
"extension": ["ts"],
"spec": "src/**/*.spec.ts"
}
Writing Mocha Tests
// src/utils/formatter.ts
export function capitalizeFirstLetter(input: string): string {
if (!input) return '';
return input.charAt(0).toUpperCase() + input.slice(1);
}
// src/utils/formatter.spec.ts
import { expect } from 'chai';
import { capitalizeFirstLetter } from './formatter';
describe('String Formatter', () => {
describe('capitalizeFirstLetter', () => {
it('should capitalize the first letter of a string', () => {
expect(capitalizeFirstLetter('hello')).to.equal('Hello');
});
it('should return an empty string when given an empty string', () => {
expect(capitalizeFirstLetter('')).to.equal('');
});
it('should not change already capitalized strings', () => {
expect(capitalizeFirstLetter('Hello')).to.equal('Hello');
});
});
});
Run the tests with:
npx mocha
Output:
String Formatter
capitalizeFirstLetter
✓ should capitalize the first letter of a string
✓ should return an empty string when given an empty string
✓ should not change already capitalized strings
3 passing (10ms)
Vitest
Vitest is a newer testing framework designed specifically for Vite-based projects. It's fast, has Jest-compatible APIs, and works very well with TypeScript.
Setting Up Vitest with TypeScript
First, install Vitest:
npm install --save-dev vitest
Add a test script to your package.json
:
{
"scripts": {
"test": "vitest"
}
}
Create a vitest.config.ts
:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
},
});
Writing Vitest Tests
// src/validation.ts
export function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
// src/validation.test.ts
import { describe, it, expect } from 'vitest';
import { isValidEmail } from './validation';
describe('Email Validation', () => {
it('should validate correct email addresses', () => {
expect(isValidEmail('[email protected]')).toBe(true);
expect(isValidEmail('[email protected]')).toBe(true);
});
it('should reject invalid email addresses', () => {
expect(isValidEmail('test@example')).toBe(false);
expect(isValidEmail('test.example.com')).toBe(false);
expect(isValidEmail('@example.com')).toBe(false);
expect(isValidEmail('')).toBe(false);
});
});
Run tests using:
npm test
Output:
✓ src/validation.test.ts (2 tests) 2ms
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 10:00:00
Duration 200ms
Cypress for End-to-End Testing
Cypress is a powerful end-to-end testing framework that can be used with TypeScript.
Setting Up Cypress with TypeScript
Install Cypress and TypeScript support:
npm install --save-dev cypress @types/cypress
Create a cypress/tsconfig.json
file:
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress", "node"]
},
"include": ["**/*.ts"]
}
Writing Cypress Tests
Create a test file in cypress/e2e/
:
// cypress/e2e/homepage.cy.ts
describe('Homepage', () => {
beforeEach(() => {
cy.visit('/');
});
it('should display the title correctly', () => {
cy.get('h1').should('have.text', 'Welcome to My App');
});
it('should navigate to About page when clicked', () => {
cy.get('nav')
.contains('About')
.click();
cy.url().should('include', '/about');
cy.get('h1').should('have.text', 'About Us');
});
it('should submit the contact form', () => {
cy.get('nav')
.contains('Contact')
.click();
cy.get('input[name="name"]').type('John Doe');
cy.get('input[name="email"]').type('[email protected]');
cy.get('textarea[name="message"]').type('This is a test message');
cy.get('form').submit();
cy.get('.success-message').should('be.visible');
});
});
Advanced Testing Patterns
Mocking in TypeScript Tests
Mocking is an essential part of testing, especially when dealing with external dependencies. Here's how to use mocking with Jest:
// src/services/user-service.ts
import { ApiClient } from './api-client';
export interface User {
id: number;
name: string;
email: string;
}
export class UserService {
private apiClient: ApiClient;
constructor(apiClient: ApiClient) {
this.apiClient = apiClient;
}
async getUser(id: number): Promise<User> {
return this.apiClient.get<User>(`/users/${id}`);
}
}
// src/services/__tests__/user-service.test.ts
import { UserService, User } from '../user-service';
import { ApiClient } from '../api-client';
// Mock the ApiClient
jest.mock('../api-client');
const MockedApiClient = ApiClient as jest.MockedClass<typeof ApiClient>;
describe('UserService', () => {
beforeEach(() => {
MockedApiClient.mockClear();
});
it('should fetch user by id', async () => {
// Set up mock data
const mockUser: User = {
id: 1,
name: 'John Doe',
email: '[email protected]'
};
// Configure mock to return our mock user
const mockGet = jest.fn().mockResolvedValue(mockUser);
MockedApiClient.prototype.get = mockGet;
// Create service with mocked API client
const userService = new UserService(new ApiClient());
const user = await userService.getUser(1);
// Assertions
expect(mockGet).toHaveBeenCalledWith('/users/1');
expect(user).toEqual(mockUser);
});
});
Testing Asynchronous Code
TypeScript applications often include asynchronous operations. Testing these operations requires special handling:
// src/data/product-repository.ts
export interface Product {
id: string;
name: string;
price: number;
}
export class ProductRepository {
private products: Product[] = [
{ id: '1', name: 'Laptop', price: 999 },
{ id: '2', name: 'Phone', price: 699 },
{ id: '3', name: 'Tablet', price: 499 }
];
async findById(id: string): Promise<Product | undefined> {
// Simulate database delay
await new Promise(resolve => setTimeout(resolve, 100));
return this.products.find(p => p.id === id);
}
async findAll(): Promise<Product[]> {
// Simulate database delay
await new Promise(resolve => setTimeout(resolve, 100));
return [...this.products];
}
}
// src/data/__tests__/product-repository.test.ts
import { ProductRepository } from '../product-repository';
describe('ProductRepository', () => {
let repository: ProductRepository;
beforeEach(() => {
repository = new ProductRepository();
});
it('should find a product by id', async () => {
const product = await repository.findById('2');
expect(product).toBeDefined();
expect(product?.name).toBe('Phone');
});
it('should return undefined for non-existent product', async () => {
const product = await repository.findById('999');
expect(product).toBeUndefined();
});
it('should return all products', async () => {
const products = await repository.findAll();
expect(products).toHaveLength(3);
expect(products[0].name).toBe('Laptop');
});
});
Test-Driven Development with TypeScript
Test-Driven Development (TDD) is a development methodology where you write tests before implementing the actual code. Here's a simple example of how to practice TDD with TypeScript:
- Write a failing test for a calculator's multiply function:
// src/calculator.test.ts
import { Calculator } from './calculator';
describe('Calculator', () => {
let calculator: Calculator;
beforeEach(() => {
calculator = new Calculator();
});
describe('multiply', () => {
it('should multiply two positive numbers correctly', () => {
expect(calculator.multiply(2, 3)).toBe(6);
});
it('should handle multiplication with zero', () => {
expect(calculator.multiply(5, 0)).toBe(0);
expect(calculator.multiply(0, 5)).toBe(0);
});
it('should handle negative numbers', () => {
expect(calculator.multiply(-2, 3)).toBe(-6);
expect(calculator.multiply(2, -3)).toBe(-6);
expect(calculator.multiply(-2, -3)).toBe(6);
});
});
});
- Implement the Calculator class to make the tests pass:
// src/calculator.ts
export class Calculator {
add(a: number, b: number): number {
return a + b;
}
subtract(a: number, b: number): number {
return a - b;
}
multiply(a: number, b: number): number {
return a * b;
}
divide(a: number, b: number): number {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
}
- Run the tests to make sure they pass.
Testing Best Practices
1. Keep Tests Simple and Focused
Each test should focus on one specific behavior or aspect of functionality.
// Bad: Testing multiple behaviors in one test
test('user authentication works', () => {
// Tests login
// Tests logout
// Tests password reset
});
// Good: Separate tests for different behaviors
test('user can log in with valid credentials', () => {
// Only test login functionality
});
test('user can log out successfully', () => {
// Only test logout functionality
});
test('user can reset password', () => {
// Only test password reset
});
2. Use Descriptive Test Names
Test names should clearly describe what is being tested and the expected outcome.
// Less descriptive:
test('logout', () => { /* ... */ });
// More descriptive:
test('user should be redirected to login page after logout', () => { /* ... */ });
3. Set Up Test Data Appropriately
Use beforeEach
and beforeAll
to set up test data, and afterEach
and afterAll
to clean up.
describe('ShoppingCart', () => {
let cart: ShoppingCart;
beforeEach(() => {
cart = new ShoppingCart();
});
test('should add items to the cart', () => {
cart.addItem({ id: '1', name: 'Product', price: 10 });
expect(cart.getItemCount()).toBe(1);
});
test('should calculate total correctly', () => {
cart.addItem({ id: '1', name: 'Product', price: 10 });
cart.addItem({ id: '2', name: 'Another Product', price: 20 });
expect(cart.getTotal()).toBe(30);
});
});
4. Use Test Coverage Tools
TypeScript projects can benefit from code coverage tools to identify untested parts of your code:
npx jest --coverage
This will generate a coverage report showing which parts of your code are covered by tests.
Visualizing Testing Workflow
Here's a simplified diagram of a typical TypeScript testing workflow:
Summary
In this guide, we've explored several TypeScript testing frameworks:
- Jest: A complete and easy-to-configure testing solution with excellent TypeScript support.
- Mocha & Chai: A flexible combination that gives you more control over your testing setup.
- Vitest: A modern, fast alternative specially designed for Vite projects.
- Cypress: A powerful end-to-end testing tool to test your application from a user's perspective.
We've also covered advanced testing techniques like mocking, testing asynchronous code, and test-driven development. Remember that good tests are:
- Fast to run
- Independent of each other
- Repeatable
- Self-validating
- Timely (written at the right time)
By incorporating these testing frameworks and practices into your TypeScript projects, you'll be able to catch bugs early, refactor with confidence, and maintain high-quality code.
Additional Resources
- Jest Documentation
- TypeScript Testing Handbook
- Testing React with TypeScript
- Cypress TypeScript Documentation
Exercises
-
Set up Jest with TypeScript in a new project and write tests for a simple string utility library with functions like
capitalize
,reverse
, andcountWords
. -
Create a React component with TypeScript that displays a counter with increment and decrement buttons. Write tests using React Testing Library to verify the component's behavior.
-
Implement a simple async data fetching function that retrieves user data from a mock API. Write tests that use mocks to verify the function behaves correctly for both successful and failed requests.
-
Practice TDD by writing tests first for a shopping cart module that should handle adding items, removing items, and calculating the total price.
-
Set up Cypress with TypeScript and write an end-to-end test for a simple form submission, verifying that the form data is submitted correctly and success messages are displayed.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)