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

Что такое Contract Testing?

2.0 Middle🔥 141 комментариев
#REST API и микросервисы#Тестирование

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

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

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

Что такое Contract Testing?

Определение

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

Проблема, которую решает Contract Testing

В микросервисной архитектуре возникает проблема:

  • Integration тесты дорогие — нужно запускать все сервисы
  • Хрупкие тесты — зависят от состояния других сервисов
  • Медленные — ждут ответа от реальных сервисов
  • Разработчики разных сервисов работают независимо — легко сломать совместимость
  • Неясно, какой сервис виноват при падении теста
Без Contract Testing:
┌──────────────┐         ┌──────────────┐
│   Service A  │────────→│   Service B  │
│  (Consumer)  │         │  (Provider)  │
└──────────────┘         └──────────────┘
       ↓                        ↓
   Full E2E Test         Full E2E Test
   (дорого, медленно)

Contract Testing решение

С Contract Testing:
┌──────────────┐                    ┌──────────────┐
│   Service A  │                    │   Service B  │
│  (Consumer)  │  ← Contract →      │  (Provider)  │
└──────────────┘      (mock)        └──────────────┘
       ↓                                    ↓
   Consumer Tests              Provider Tests
   (быстро, независимо)        (проверка совместимости)

Типы Contract Testing

1. Synchronous Contract Testing (для REST/gRPC)

Тестирование request-response контракта между двумя сервисами.

// Consumer тест: проверяем, что используем правильный API
@ExtendWith(PactConsumerTestExt.class)
public class OrderServiceConsumerTest {
    
    @Pact(consumer = "OrderService", provider = "PaymentService")
    public V4Pact createPact(PactBuilder builder) {
        return builder
                .uponReceiving("a request to process payment")
                .path("/api/v1/payments")
                .method("POST")
                .headers("Content-Type", "application/json")
                .body(matchJson(map(
                    "amount", 100.50,
                    "currency", "USD",
                    "orderId", "ORD-123"
                )))
                .willRespondWith(200)
                .body(matchJson(map(
                    "transactionId", "TXN-456",
                    "status", "COMPLETED"
                )))
                .toPact();
    }
    
    @Test
    void testProcessPayment(MockServer mockServer) throws IOException {
        PaymentClient client = new PaymentClient(mockServer.getUrl());
        
        Payment payment = Payment.builder()
                .amount(100.50)
                .currency("USD")
                .orderId("ORD-123")
                .build();
        
        PaymentResponse response = client.processPayment(payment);
        
        assertThat(response.getTransactionId()).isEqualTo("TXN-456");
        assertThat(response.getStatus()).isEqualTo("COMPLETED");
    }
}

// Provider тест: проверяем, что API работает правильно
@ExtendWith(PactVerificationTestExt.class)
public class PaymentServiceProviderTest {
    
    @BeforeEach
    void setup() {
        // Стартуем PaymentService на определенном порту
        PaymentService.start(8080);
    }
    
    @TestTemplate
    @PactDirectory("pacts")
    void pactVerificationTestTemplate(PactVerificationContext context) {
        context.verifyInteraction();
    }
}

Этот пример использует Pact — популярный фреймворк для Contract Testing.

2. Asynchronous Contract Testing (для Event-Driven)

Для систем с асинхронной коммуникацией (очереди сообщений, Kafka).

// Consumer: слушаем события OrderCreated
@ExtendWith(PactConsumerTestExt.class)
public class InventoryServiceConsumerTest {
    
    @Pact(consumer = "InventoryService", provider = "OrderService")
    public V4Pact createEventPact(PactBuilder builder) {
        return builder
                .uponReceiving("an OrderCreated event")
                .aMessageInteraction()
                .withContent(matchJson(map(
                    "eventType", "ORDER_CREATED",
                    "orderId", "ORD-123",
                    "items", array(
                        map("sku", "ITEM-1", "quantity", 5)
                    ),
                    "timestamp", "2024-03-23T10:00:00Z"
                )))
                .toPact();
    }
    
    @Test
    void testOrderCreatedEventProcessing() {
        // Получили событие
        String eventPayload = "{\"eventType\":\"ORDER_CREATED\", ...}";
        
        // Обрабатываем его
        InventoryService service = new InventoryService();
        service.processOrderCreatedEvent(eventPayload);
        
        // Проверяем, что инвентарь обновился
        assertThat(service.getInventory("ITEM-1")).isEqualTo(95);
    }
}

Contract Testing с Pact

Pact — стандартный инструмент для Contract Testing:

import au.com.dius.pact.consumer.dsl.PactBuilder;
import au.com.dius.pact.core.model.V4Pact;

public class UserServiceContractTest {
    
    @Pact(consumer = "UserPortal", provider = "UserAPI")
    public V4Pact getUserPact(PactBuilder builder) {
        return builder
                .uponReceiving("a request to get user by ID")
                .path("/api/v1/users/123")
                .method("GET")
                .willRespondWith(200)
                .body(matchJson(map(
                    "id", 123,
                    "name", "John Doe",
                    "email", "john@example.com",
                    "roles", array("ADMIN", "USER")
                )))
                .toPact();
    }
    
    @Test
    @PactTestFor(pactMethod = "getUserPact")
    void testGetUserById(MockServer mockServer) throws IOException {
        UserClient client = new UserClient(mockServer.getUrl());
        
        User user = client.getUserById(123);
        
        assertThat(user.getId()).isEqualTo(123);
        assertThat(user.getName()).isEqualTo("John Doe");
        assertThat(user.getRoles()).contains("ADMIN", "USER");
    }
}

Процесс Contract Testing

1. Consumer разработчик пишет тесты контракта
   ↓
2. Тесты запускаются с MockServer
   ↓
3. Контракт сохраняется в файл (pact файл)
   ↓
4. Контракт отправляется на Pact Broker (центральный репо контрактов)
   ↓
5. Provider разработчик скачивает контракт
   ↓
6. Provider запускает свои тесты против контракта
   ↓
7. Если тесты прошли → контракт совместим
   ↓
8. Если нет → нужно обновить API

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

// 1. Быстрые тесты (mock вместо реального сервиса)
@Test
void fastConsumerTest() {
    // Вместо
    // PaymentService.start();  // 5 секунд
    
    // Используем
    PaymentClient client = new PaymentClient(mockServer);
    // Готово мгновенно
}

// 2. Независимая разработка
// Consumer разработчик может тестировать свою логику
// даже если Provider API еще не готов

// 3. Раннее обнаружение несовместимости
// Ошибки в контракте поймаются до merge в main

// 4. Документация
// Pact файлы служат живой документацией интеграции

Consumer-Driven Contracts (CDC)

Специальный подход Contract Testing, где consumer определяет требования:

1. Consumer говорит: "Мне нужен такой API"
2. Пишет consumer-contract тест
3. Provider смотрит на требования consumer
4. Реализует API согласно контракту

Инструменты

  • Pact — самый популярный (Java, Go, JS, Ruby, .NET, Python)
  • Spring Cloud Contract — интеграция с Spring
  • Pacto — альтернатива
  • Stubby4j — для создания mock сервисов

Лучшие практики

  1. Пиши realistic контракты — только то, что реально используется
  2. Версионируй контракты — отслеживай изменения
  3. Используй matchers — не захардкодь значения
  4. Один контракт на одну интеграцию — ясная структура
  5. Интегрируй в CI/CD — автоматическая проверка

Итоговый вывод

Contract Testing — это essential практика в микросервисной архитектуре. Она позволяет разработчикам разных команд работать независимо и параллельно, при этом гарантируя совместимость интерфейсов. Contract Testing значительно ускоряет разработку и снижает риск интеграции, что критично для больших систем.