← Назад к вопросам

Как было сделано тестирование в проекте?

2.0 Middle🔥 201 комментариев
#Тестирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Стратегия тестирования в backend проектах

Тестирование — это критически важная часть качественной разработки. За 10+ лет я разработал comprehensive testing strategy для Node.js проектов, которая обеспечивает надёжность и maintainability кода.

Пирамида тестирования

         E2E Tests
        /          \
      Integration Tests
     /              \
   Unit Tests
  /              \
Build & Lint

Распределение по количеству:

  • Unit тесты — 70% (быстро, изолировано)
  • Integration тесты — 20% (проверяют взаимодействие)
  • E2E тесты — 10% (полный flow, медленнее)

1. Unit тесты (Jest/Vitest)

Единичные тесты для отдельных функций и классов.

// src/services/user.service.ts
export class UserService {
  constructor(private userRepository: UserRepository) {}

  async createUser(email: string, password: string): Promise<User> {
    if (!this.isValidEmail(email)) {
      throw new Error('Invalid email');
    }
    const hashedPassword = await this.hashPassword(password);
    return this.userRepository.create({ email, password: hashedPassword });
  }

  private isValidEmail(email: string): boolean {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }

  private async hashPassword(password: string): Promise<string> {
    return bcrypt.hash(password, 10);
  }
}

// src/services/user.service.test.ts
import { UserService } from './user.service';
import { UserRepository } from './user.repository';

describe('UserService', () => {
  let service: UserService;
  let mockRepository: jest.Mocked<UserRepository>;

  beforeEach(() => {
    mockRepository = {
      create: jest.fn(),
    } as any;
    service = new UserService(mockRepository);
  });

  describe('createUser', () => {
    it('should create user with valid email and password', async () => {
      // Arrange
      const email = 'test@example.com';
      const password = 'secure123';
      const expectedUser = { id: '1', email, password: 'hashed' };
      mockRepository.create.mockResolvedValue(expectedUser);

      // Act
      const result = await service.createUser(email, password);

      // Assert
      expect(result).toEqual(expectedUser);
      expect(mockRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({ email })
      );
    });

    it('should throw error for invalid email', async () => {
      // Arrange & Act & Assert
      await expect(service.createUser('invalid-email', 'password123'))
        .rejects
        .toThrow('Invalid email');
      expect(mockRepository.create).not.toHaveBeenCalled();
    });

    it('should hash password before saving', async () => {
      // Arrange
      const email = 'test@example.com';
      const password = 'mypassword';
      mockRepository.create.mockResolvedValue({
        id: '1',
        email,
        password: expect.stringMatching(/^\$2[aby]\$/), // bcrypt hash pattern
      });

      // Act
      await service.createUser(email, password);

      // Assert
      const callArgs = mockRepository.create.mock.calls[0][0];
      expect(callArgs.password).not.toBe(password); // Не исходный пароль
    });
  });
});

Ключевые моменты:

  • AAA Pattern (Arrange, Act, Assert)
  • Моки для зависимостей (mockRepository)
  • Граничные случаи (invalid email)
  • Проверка побочных эффектов (was repository.create called?)

2. Integration тесты

Тестируют взаимодействие компонентов (сервис + репозиторий + БД).

// src/services/user.service.integration.test.ts
import { TypeOrmModule } from '@nestjs/typeorm';
import { Test } from '@nestjs/testing';
import { DataSource } from 'typeorm';

describe('UserService Integration', () => {
  let service: UserService;
  let repository: Repository<User>;
  let dataSource: DataSource;

  beforeAll(async () => {
    const moduleRef = await Test.createTestingModule({
      imports: [
        TypeOrmModule.forRoot({
          type: 'sqlite',
          database: ':memory:',
          entities: [User],
          synchronize: true,
        }),
        TypeOrmModule.forFeature([User]),
      ],
      providers: [UserService, UserRepository],
    }).compile();

    service = moduleRef.get<UserService>(UserService);
    repository = moduleRef.get<Repository<User>>(
      getRepositoryToken(User)
    );
    dataSource = moduleRef.get<DataSource>(DataSource);
  });

  afterEach(async () => {
    // Очистка БД после каждого теста
    await repository.clear();
  });

  afterAll(async () => {
    await dataSource.destroy();
  });

  it('should create user and persist to database', async () => {
    // Arrange
    const email = 'integration@test.com';
    const password = 'secure123';

    // Act
    const user = await service.createUser(email, password);

    // Assert
    const savedUser = await repository.findOne({ where: { id: user.id } });
    expect(savedUser).toBeDefined();
    expect(savedUser?.email).toBe(email);
  });

  it('should not create duplicate emails', async () => {
    // Arrange
    const email = 'duplicate@test.com';
    await service.createUser(email, 'password1');

    // Act & Assert
    await expect(service.createUser(email, 'password2'))
      .rejects
      .toThrow('Email already exists');
  });
});

Особенности:

  • Реальная БД (SQLite in-memory для тестов)
  • Полный lifecycle (создание, сохранение, поиск)
  • Проверка ограничений (unique email)

3. E2E тесты (API testing)

Тестируют полный HTTP flow от request до response.

// src/users/users.controller.e2e.test.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { UsersModule } from './users.module';

describe('Users API (e2e)', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [UsersModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  afterAll(async () => {
    await app.close();
  });

  describe('POST /api/v1/users (Create user)', () => {
    it('should create user with valid data', async () => {
      const createUserDto = {
        email: 'newuser@test.com',
        password: 'SecurePassword123!',
        name: 'John Doe',
      };

      const response = await request(app.getHttpServer())
        .post('/api/v1/users')
        .send(createUserDto)
        .expect(201);

      expect(response.body).toEqual(
        expect.objectContaining({
          id: expect.any(String),
          email: createUserDto.email,
          name: createUserDto.name,
        })
      );
      // Важно: пароль НЕ возвращается в ответе!
      expect(response.body.password).toBeUndefined();
    });

    it('should validate email format', async () => {
      const response = await request(app.getHttpServer())
        .post('/api/v1/users')
        .send({
          email: 'invalid-email',
          password: 'SecurePassword123!',
          name: 'Jane',
        })
        .expect(400);

      expect(response.body.message).toContain('Invalid email');
    });

    it('should return 409 for duplicate email', async () => {
      const email = 'existing@test.com';
      const dto = { email, password: 'pass123', name: 'User' };

      // Первое создание
      await request(app.getHttpServer())
        .post('/api/v1/users')
        .send(dto)
        .expect(201);

      // Второе с тем же email
      const response = await request(app.getHttpServer())
        .post('/api/v1/users')
        .send(dto)
        .expect(409);

      expect(response.body.message).toContain('Email already exists');
    });
  });

  describe('GET /api/v1/users/:id (Get user)', () => {
    let userId: string;

    beforeAll(async () => {
      const response = await request(app.getHttpServer())
        .post('/api/v1/users')
        .send({
          email: 'getuser@test.com',
          password: 'pass123',
          name: 'Get Test',
        });
      userId = response.body.id;
    });

    it('should get user by id', async () => {
      const response = await request(app.getHttpServer())
        .get(`/api/v1/users/${userId}`)
        .expect(200);

      expect(response.body).toEqual(
        expect.objectContaining({
          id: userId,
          email: 'getuser@test.com',
          name: 'Get Test',
        })
      );
    });

    it('should return 404 for non-existent user', async () => {
      const fakeId = 'non-existent-id';
      await request(app.getHttpServer())
        .get(`/api/v1/users/${fakeId}`)
        .expect(404);
    });
  });
});

4. Database тесты (с миграциями)

// test/database.setup.ts
import { DataSource } from 'typeorm';
import { runMigrations, revertMigrations } from '../migrations';

export async function setupTestDatabase(): Promise<DataSource> {
  const dataSource = new DataSource({
    type: 'postgres',
    host: process.env.TEST_DB_HOST || 'localhost',
    port: parseInt(process.env.TEST_DB_PORT || '5432'),
    username: process.env.TEST_DB_USER || 'test',
    password: process.env.TEST_DB_PASSWORD || 'test',
    database: process.env.TEST_DB_NAME || 'testdb',
    entities: [/* ... */],
    dropSchema: true, // Очистить перед запуском
    synchronize: false,
    migrationsRun: true,
    migrations: ['migrations/*.ts'],
  });

  await dataSource.initialize();
  return dataSource;
}

// Использование
beforeAll(async () => {
  dataSource = await setupTestDatabase();
});

afterAll(async () => {
  await dataSource.destroy();
});

5. Тестирование асинхронного кода

describe('Async operations', () => {
  it('should handle promise', async () => {
    const promise = service.fetchUser(123);
    const user = await promise;
    expect(user.id).toBe(123);
  });

  it('should handle timeout', async () => {
    jest.useFakeTimers();
    const promise = service.delayedOperation();
    
    // Пропускаем время
    jest.advanceTimersByTime(5000);
    
    expect(promise).resolves.toBeDefined();
    jest.useRealTimers();
  });

  it('should retry on failure', async () => {
    const spy = jest.spyOn(service, 'unreliableApi');
    spy.mockRejectedValueOnce(new Error('Network error'))
       .mockResolvedValueOnce({ data: 'success' });

    const result = await service.apiWithRetry();
    expect(result.data).toBe('success');
    expect(spy).toHaveBeenCalledTimes(2);
  });
});

6. Coverage requirements

# package.json
"scripts": {
  "test": "jest --coverage",
  "test:watch": "jest --watch"
}

# jest.config.js
module.exports = {
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.test.ts',
    '!src/**/index.ts',
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

7. Fixtures и test data

// test/fixtures/users.fixture.ts
export const validUserData = {
  email: 'test@example.com',
  password: 'SecurePass123!',
  name: 'Test User',
};

export const invalidUsers = [
  { email: 'invalid', password: 'pass' },    // Invalid email
  { email: 'test@test.com', password: '' },  // Empty password
  { email: '', password: 'pass' },            // Empty email
];

// Использование
it('should reject invalid users', async () => {
  for (const invalidUser of invalidUsers) {
    await expect(service.createUser(
      invalidUser.email,
      invalidUser.password
    )).rejects.toThrow();
  }
});

8. Тестирование HTTP клиентов (VCR.py / nock)

// При первом запуске записывает реальный HTTP ответ
// При последующих запусках использует записанные данные

import nock from 'nock';

it('should call external API', async () => {
  nock('https://api.external.com')
    .get('/data/123')
    .reply(200, { id: 123, data: 'mocked response' });

  const result = await externalService.fetchData(123);
  expect(result.data).toBe('mocked response');
});

Чеклист для тестирования проекта

Структура:

  • Unit тесты для каждого сервиса
  • Integration тесты для сложного взаимодействия
  • E2E тесты для API endpoints
  • Фиксчуры для test data

Coverage:

  • Минимум 80% для production кода
  • 100% для критичных операций (auth, payment)

Скорость:

  • Unit тесты < 100ms
  • Integration < 1s
  • E2E тесты группировать (max 10s per suite)

Мейнтейнанс:

  • Descriptive test names
  • One assertion per test (обычно)
  • Clear setup/teardown
  • Изолированность (не зависят от порядка)

CI/CD интеграция:

  • Автоматический запуск на каждый commit
  • Fail если coverage < threshold
  • Reportы по результатам

Итоги

Качественное тестирование: ✓ Предотвращает баги в production ✓ Упрощает рефакторинг ✓ Служит документацией ✓ Даёт уверенность в коде ✓ Экономит время при поддержке

Я никогда не пишу production код без тестов — это стандарт профессиональной разработки.

Как было сделано тестирование в проекте? | PrepBro