API Testing стратегии: Unit, Integration, Contract, E2E

← Вернуться к статьям

Введение

Тестирование API — критически важная часть разработки качественного программного обеспечения. Правильный выбор стратегии тестирования определяет скорость разработки, стабильность системы и уверенность в качестве кода.

В этом руководстве мы разберем основные стратегии тестирования API: Unit тестирование, Integration тестирование, Contract testing и E2E тестирование. Узнаем, когда какую стратегию использовать, и как они работают вместе в рамках API Testing Pyramid.

✅ Что вы узнаете:

📋 Содержание

API Testing Pyramid 🏗️

API Testing Pyramid — это модель, показывающая оптимальное распределение тестов по уровням. Основание пирамиды — быстрые и дешевые тесты, вершина — медленные и дорогие, но критически важные.

E2E Tests

Меньше всего
Медленные
Дорогие

Contract Tests

Среднее количество
Средняя скорость

Integration Tests

Больше
Быстрее

Unit Tests

Больше всего
Самые быстрые
Самые дешевые

✅ Принципы Testing Pyramid:

Unit тестирование API 🧪

Unit тестирование — это тестирование отдельных функций, методов или компонентов API в изоляции от внешних зависимостей (БД, внешние сервисы, файловая система).

Характеристики Unit тестов:

Пример Unit теста (Node.js + Jest)

// userService.test.js
const UserService = require('./userService');
const UserRepository = require('./userRepository');

// Мокаем зависимость
jest.mock('./userRepository');

describe('UserService', () => {
  let userService;
  let mockUserRepository;

  beforeEach(() => {
    mockUserRepository = {
      findById: jest.fn(),
      create: jest.fn(),
      update: jest.fn()
    };
    UserRepository.mockImplementation(() => mockUserRepository);
    userService = new UserService();
  });

  describe('getUser', () => {
    it('должен вернуть пользователя по ID', async () => {
      // Arrange
      const userId = '123';
      const mockUser = { id: userId, name: 'John', email: 'john@example.com' };
      mockUserRepository.findById.mockResolvedValue(mockUser);

      // Act
      const result = await userService.getUser(userId);

      // Assert
      expect(result).toEqual(mockUser);
      expect(mockUserRepository.findById).toHaveBeenCalledWith(userId);
      expect(mockUserRepository.findById).toHaveBeenCalledTimes(1);
    });

    it('должен выбросить ошибку если пользователь не найден', async () => {
      // Arrange
      mockUserRepository.findById.mockResolvedValue(null);

      // Act & Assert
      await expect(userService.getUser('999')).rejects.toThrow('User not found');
    });
  });

  describe('validateEmail', () => {
    it('должен вернуть true для валидного email', () => {
      expect(userService.validateEmail('test@example.com')).toBe(true);
    });

    it('должен вернуть false для невалидного email', () => {
      expect(userService.validateEmail('invalid-email')).toBe(false);
    });
  });
});

Пример Unit теста (Python + pytest)

# test_user_service.py
import pytest
from unittest.mock import Mock, patch
from user_service import UserService

class TestUserService:
    @pytest.fixture
    def mock_repository(self):
        return Mock()
    
    @pytest.fixture
    def user_service(self, mock_repository):
        return UserService(mock_repository)
    
    def test_get_user_success(self, user_service, mock_repository):
        # Arrange
        user_id = '123'
        mock_user = {'id': user_id, 'name': 'John', 'email': 'john@example.com'}
        mock_repository.find_by_id.return_value = mock_user
        
        # Act
        result = user_service.get_user(user_id)
        
        # Assert
        assert result == mock_user
        mock_repository.find_by_id.assert_called_once_with(user_id)
    
    def test_get_user_not_found(self, user_service, mock_repository):
        # Arrange
        mock_repository.find_by_id.return_value = None
        
        # Act & Assert
        with pytest.raises(ValueError, match='User not found'):
            user_service.get_user('999')
    
    def test_validate_email_valid(self, user_service):
        assert user_service.validate_email('test@example.com') == True
    
    def test_validate_email_invalid(self, user_service):
        assert user_service.validate_email('invalid-email') == False

Integration тестирование 🔗

Integration тестирование — это тестирование взаимодействия компонентов API с реальными зависимостями: базой данных, внешними сервисами, файловой системой.

Характеристики Integration тестов:

Пример Integration теста (Node.js)

// user.integration.test.js
const request = require('supertest');
const app = require('./app');
const db = require('./db');

describe('User API Integration Tests', () => {
  beforeAll(async () => {
    // Подключение к тестовой БД
    await db.connect();
  });

  afterAll(async () => {
    await db.disconnect();
  });

  beforeEach(async () => {
    // Очистка БД перед каждым тестом
    await db.clear();
  });

  describe('POST /api/users', () => {
    it('должен создать нового пользователя', async () => {
      const userData = {
        name: 'John Doe',
        email: 'john@example.com'
      };

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

      expect(response.body).toHaveProperty('id');
      expect(response.body.name).toBe(userData.name);
      expect(response.body.email).toBe(userData.email);
    });

    it('должен вернуть ошибку при дублировании email', async () => {
      const userData = {
        name: 'John Doe',
        email: 'john@example.com'
      };

      // Создаем первого пользователя
      await request(app)
        .post('/api/users')
        .send(userData);

      // Пытаемся создать второго с тем же email
      const response = await request(app)
        .post('/api/users')
        .send(userData)
        .expect(400);

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

  describe('GET /api/users/:id', () => {
    it('должен вернуть пользователя по ID', async () => {
      // Создаем пользователя
      const createResponse = await request(app)
        .post('/api/users')
        .send({ name: 'John', email: 'john@example.com' });

      const userId = createResponse.body.id;

      // Получаем пользователя
      const response = await request(app)
        .get(`/api/users/${userId}`)
        .expect(200);

      expect(response.body.id).toBe(userId);
      expect(response.body.name).toBe('John');
    });
  });
});

Contract Testing 📋

Contract Testing — это подход к тестированию, при котором проверяется соответствие API контракту между потребителем (consumer) и провайдером (provider) сервиса.

✅ Преимущества Contract Testing:

Pact (Consumer-Driven Contract Testing)

Pact — популярный инструмент для consumer-driven contract testing. Consumer определяет ожидаемый контракт, а provider проверяет соответствие.

Consumer Side (Node.js + Pact)

// user-api.consumer.test.js
const { Pact } = require('@pact-foundation/pact');
const path = require('path');
const UserApiClient = require('./userApiClient');

describe('User API Consumer', () => {
  const provider = new Pact({
    consumer: 'UserService',
    provider: 'UserAPI',
    port: 1234,
    log: path.resolve(process.cwd(), 'logs', 'pact.log'),
    dir: path.resolve(process.cwd(), 'pacts'),
    logLevel: 'INFO'
  });

  beforeAll(() => provider.setup());
  afterEach(() => provider.verify());
  afterAll(() => provider.finalize());

  describe('getUser', () => {
    it('должен получить пользователя по ID', async () => {
      const expectedUser = {
        id: '123',
        name: 'John Doe',
        email: 'john@example.com'
      };

      await provider.addInteraction({
        state: 'user exists',
        uponReceiving: 'a request for user by ID',
        withRequest: {
          method: 'GET',
          path: '/api/users/123',
          headers: {
            'Accept': 'application/json'
          }
        },
        willRespondWith: {
          status: 200,
          headers: {
            'Content-Type': 'application/json'
          },
          body: expectedUser
        }
      });

      const client = new UserApiClient('http://localhost:1234');
      const user = await client.getUser('123');

      expect(user).toEqual(expectedUser);
    });
  });
});

Provider Side (Verification)

// user-api.provider.test.js
const { Verifier } = require('@pact-foundation/pact');
const path = require('path');

describe('User API Provider', () => {
  it('должен соответствовать контракту с UserService', async () => {
    const verifier = new Verifier({
      providerBaseUrl: 'http://localhost:3000',
      pactUrls: [path.resolve(process.cwd(), 'pacts', 'userservice-userapi.json')],
      provider: 'UserAPI',
      logLevel: 'INFO'
    });

    await verifier.verifyProvider();
  });
});

Spring Cloud Contract (Java)

// UserControllerTest.java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
@AutoConfigureStubRunner(
    ids = {"com.example:user-service:+:stubs:8080"},
    stubsMode = StubRunnerProperties.StubsMode.LOCAL
)
public class UserControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Test
    public void shouldReturnUserById() throws Exception {
        mockMvc.perform(get("/api/users/123")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value("123"))
                .andExpect(jsonPath("$.name").value("John Doe"));
    }
}

// Контракт (Groovy DSL)
Contract.make {
    request {
        method 'GET'
        url('/api/users/123')
    }
    response {
        status 200
        body([
            id: '123',
            name: 'John Doe',
            email: 'john@example.com'
        ])
        headers {
            contentType('application/json')
        }
    }
}

E2E тестирование 🎯

E2E (End-to-End) тестирование — это тестирование полного потока от начала до конца, включая все компоненты системы: API, база данных, внешние сервисы, фронтенд.

Характеристики E2E тестов:

⚠️ Проблемы E2E тестов:

Пример E2E теста

// user.e2e.test.js
const request = require('supertest');
const app = require('./app');

describe('User E2E Flow', () => {
  it('должен выполнить полный поток создания и получения пользователя', async () => {
    // 1. Создание пользователя
    const createResponse = await request(app)
      .post('/api/users')
      .send({
        name: 'John Doe',
        email: 'john@example.com',
        password: 'securePassword123'
      })
      .expect(201);

    const userId = createResponse.body.id;
    expect(createResponse.body).toHaveProperty('id');
    expect(createResponse.body.name).toBe('John Doe');

    // 2. Авторизация
    const authResponse = await request(app)
      .post('/api/auth/login')
      .send({
        email: 'john@example.com',
        password: 'securePassword123'
      })
      .expect(200);

    const token = authResponse.body.token;
    expect(token).toBeDefined();

    // 3. Получение профиля с токеном
    const profileResponse = await request(app)
      .get('/api/users/me')
      .set('Authorization', `Bearer ${token}`)
      .expect(200);

    expect(profileResponse.body.id).toBe(userId);
    expect(profileResponse.body.email).toBe('john@example.com');

    // 4. Обновление профиля
    const updateResponse = await request(app)
      .put('/api/users/me')
      .set('Authorization', `Bearer ${token}`)
      .send({
        name: 'John Updated'
      })
      .expect(200);

    expect(updateResponse.body.name).toBe('John Updated');
  });
});

Когда какую стратегию использовать? 🤔

Стратегия Когда использовать Примеры
Unit Tests Всегда, для всех функций и методов Валидация данных, бизнес-логика, утилиты
Integration Tests Ключевые интеграционные точки Работа с БД, внешние API, файловая система
Contract Tests Микросервисная архитектура Контракты между сервисами, API версионирование
E2E Tests Критические бизнес-сценарии Полный поток регистрации, оплаты, ключевые фичи
✅ Рекомендации по распределению:

Best Practices 📚

✅ Рекомендации по тестированию API:

Распространенные ошибки

❌ Чего избегать:

Заключение

Правильный выбор стратегии тестирования API критически важен для качества и скорости разработки. Следуя принципам Testing Pyramid и используя правильные инструменты, вы создадите надежную систему тестирования.

💡 Ключевые выводы:

Создайте Mock API для тестирования за 2 минуты

Хотите ускорить разработку и тестирование? Создайте Mock API с помощью LightBox API и тестируйте интеграции без необходимости настраивать сложный backend.

Попробовать бесплатно →
← Вернуться к статьям