Продвинутое тестирование в TypeScript: от Unit до E2E
Тестирование — это не просто проверка работоспособности кода, а фундамент профессиональной разработки на TypeScript. На продвинутом уровне мы переходим от простых assert-ов к построению изолированных, быстрых и надежных тестовых пирамид. В этой статье мы разберем практические подходы к unit, интеграционному и e2e-тестированию с использованием лучших инструментов экосистемы TypeScript.
1. Продвинутое Unit-тестирование с Jest и TypeScript
Unit-тесты проверяют изолированные модули. В TypeScript ключевая сложность — работа с зависимостями, типами и асинхронностью.
1.1. Моки и типизация с jest.mocked
Используйте jest.mocked для полной типизации моков. Это гарантирует, что вы не вызовете несуществующий метод.
import { fetchUserData } from './api';
import { processUser } from './userProcessor';
jest.mock('./api');
const mockedFetchUserData = jest.mocked(fetchUserData);
describe('processUser', () => {
it('должен обработать данные пользователя', async () => {
const mockUser = { id: 1, name: 'Alice', email: 'alice@test.com' };
mockedFetchUserData.mockResolvedValue(mockUser);
const result = await processUser(1);
expect(result).toEqual({
displayName: 'Alice',
contact: 'alice@test.com'
});
expect(mockedFetchUserData).toHaveBeenCalledWith(1);
});
it('должен выбросить ошибку при отсутствии email', async () => {
mockedFetchUserData.mockResolvedValue({ id: 2, name: 'Bob', email: null });
await expect(processUser(2)).rejects.toThrow('Email is required');
});
});
1.2. Тестирование сложных типов и generics
Для функций с дженериками создавайте тестовые типы-помощники, чтобы избежать дублирования.
// Код
function getFirstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
// Тест
type TestArray = number[];
const array: TestArray = [10, 20, 30];
describe('getFirstElement', () => {
it('должен вернуть первый элемент числа', () => {
const result = getFirstElement(array);
expect(result).toBe(10);
// TypeScript автоматически выводит тип number
});
it('должен вернуть undefined для пустого массива', () => {
const result = getFirstElement([]);
expect(result).toBeUndefined();
});
});
2. Интеграционное тестирование: база данных и API
Интеграционные тесты проверяют взаимодействие модулей: контроллеров, сервисов и базы данных. Для TypeScript-приложений на Node.js идеально подходит связка Jest + Supertest + Testcontainers.
2.1. Настройка тестовой базы данных с Testcontainers
Testcontainers поднимает PostgreSQL в Docker, гарантируя чистую среду для каждого теста.
import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';
import { Client } from 'pg';
let container: StartedPostgreSqlContainer;
let client: Client;
beforeAll(async () => {
container = await new PostgreSqlContainer().start();
client = new Client({
connectionString: container.getConnectionUri()
});
await client.connect();
await client.query(`
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL
);
`);
}, 30000); // таймаут 30 секунд для скачивания образа
afterAll(async () => {
await client.end();
await container.stop();
});
describe('User Repository', () => {
it('должен создать и вернуть пользователя', async () => {
const res = await client.query(
`INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *`,
['Test', 'test@example.com']
);
expect(res.rows[0].name).toBe('Test');
});
});
2.2. Тестирование REST API с Supertest
Запускаем Express-приложение и тестируем эндпоинты, используя реальную БД.
import request from 'supertest';
import { app } from '../app';
import { db } from '../database';
beforeEach(async () => {
await db.query('DELETE FROM users');
});
describe('POST /api/users', () => {
it('должен создать пользователя и вернуть 201', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'John', email: 'john@test.com' })
.expect(201);
expect(response.body).toMatchObject({
name: 'John',
email: 'john@test.com'
});
expect(response.body).toHaveProperty('id');
});
it('должен вернуть 409 при дубликате email', async () => {
await request(app)
.post('/api/users')
.send({ name: 'First', email: 'dup@test.com' });
await request(app)
.post('/api/users')
.send({ name: 'Second', email: 'dup@test.com' })
.expect(409);
});
});
3. E2E-тестирование с Playwright и TypeScript
End-to-end тесты симулируют действия реального пользователя в браузере. Playwright — лучший выбор для TypeScript благодаря нативной поддержке типов и автогенерации селекторов.
3.1. Структура Page Object Model (POM)
Используйте паттерн Page Object для переиспользования селекторов и логики.
// pages/LoginPage.ts
import { Page } from '@playwright/test';
export class LoginPage {
constructor(private page: Page) {}
async navigate() {
await this.page.goto('/login');
}
async login(username: string, password: string) {
await this.page.fill('#username', username);
await this.page.fill('#password', password);
await this.page.click('button[type="submit"]');
}
async getErrorMessage() {
return this.page.textContent('.error-message');
}
}
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('успешный вход в систему', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.navigate();
await loginPage.login('admin', 'password123');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toContainText('Dashboard');
});
test('отображение ошибки при неверном пароле', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.navigate();
await loginPage.login('admin', 'wrong');
const error = await loginPage.getErrorMessage();
expect(error).toBe('Invalid credentials');
});