Sobes.tech
Middle+

Tell about contract testing of a microservice.

sobes.tech AI

Answer from AI

Contract testing focuses on verifying the interaction between microservices at their API (contract) level. The goal is to ensure that the consumer (the service using the API) and the provider (the service offering the API) adhere to a specific "contract" (request/response schema, data format, expectations).

Main principles:

  • Consumer-driven: Tests are based on the consumer's expectations. The consumer defines what data and formats it needs.
  • Bidirectional testing: It checks both the consumer side (can it work with the current contract) and the provider side (does its implementation meet the consumer's expectations).
  • Speed: Tests are fast, often part of CI/CD pipelines, as they do not require deploying all dependent services.

The process usually includes:

  1. The consumer creates tests describing its expectations from the provider's API. These tests generate a "contract".
  2. The contract is published (e.g., in a centralized repository).
  3. The provider uses the published contract to verify its API implementation. It runs tests (usually on the provider side) to check if its API conforms to the contract.
  4. If there is a discrepancy between the consumer's expectations and the provider's implementation, the test fails, indicating a contract violation.

Tools:

  • Pact (most popular)
  • Spring Cloud Contract
  • Swagger/OpenAPI with validation tools

Advantages:

  • Early detection of integration errors: Compatibility issues are identified before deployment.
  • Reduces the need for integration tests: Decreases reliance on complex and slow end-to-end tests.
  • Independent testing: Allows testing services independently of each other.
  • Clear expectations: Explicitly defines the contract between services.

Disadvantages:

  • Does not fully replace integration tests (only verifies interaction at the API level).
  • Requires discipline in versioning contracts.
  • Can be more complex for services with very intricate contracts.

Example using Pact:

  1. The consumer (e.g., Order Service) uses the Pact library to create consumer-side tests.
    // Example consumer test with Pact JVM
    @ExtendWith(PactConsumerTestExt.class)
    @PactTestFor(providerName = "ProductService", port = "8080")
    public class ProductServiceContractTest {
    
        @Pact(consumer = "OrderService")
        public RequestResponsePact createPact(PactDslWithRequest r) {
            return r.given("a product with id 1 exists")
                    .uponReceiving("a request for product by id")
                    .path("/products/1")
                    .method("GET")
                    .willRespondWith()
                    .status(200)
                    .headers(Map.of("Content-Type", "application/json"))
                    .body(new PactDslJsonBody()
                            .stringValue("id", "1")
                            .stringValue("name", "Laptop")
                            .numberType("price", 1200.00))
                    .toPact();
        }
    
        @Test
        void testGetProductById(MockServer mockServer) throws IOException {
            // Logic to call provider API via mockServer
            // Verify that the consumer correctly handles the mock response
            HttpResponse response = Request.Get(mockServer.getUrl() + "/products/1").execute().returnResponse();
            assertThat(response.getStatusLine().getStatusCode(), equalTo(200));
        }
    }
    
  2. When running consumer-side tests, a contract file (.json) is generated.
  3. This contract file is published to a Pact Broker or another centralized repository.
  4. The provider (Product Service) uses the Pact library (e.g., Pact Provider Verifier) to verify its API implementation based on the contract received from the Pact Broker.
    // Example provider test with Pact Provider Verifier
    @Provider("ProductService")
    @PactFolder("relative/path/to/pacts") // or use Pact Broker
    public class ProductServiceVerificationTest {
    
        @TestTemplate
        @ExtendWith(PactVerificationInvocationContextProvider.class)
        public void verifyPact(PactVerificationContext context) {
            context.verifyInteraction();
        }
    
        @State("a product with id 1 exists")
        public void productWithId1Exists() {
            // Logic to prepare provider data state for the test
        }
    
        // Add provider URL where the service is running
        @BeforeEach
        void beforeEach(PactVerificationContext context) {
            context.setTarget(new HttpTestTarget("localhost", 8080));
        }
    }
    
  5. The verifier on the provider side calls the actual provider API according to the contract specification and compares the response with the expected one in the contract.

Contract testing is an important practice to ensure the reliability and maintainability of microservice-based systems.