TDD - Microservice Testing
Many developers are writing integration tests the wrong way - they write Broad Integration Tests, which have similar limitations as E2E Tests - limited scenarios, brittle & fragile.
🚀 Do you want stop regression bugs escaping to production? Join me at my next FREE live event: “Stop Breaking Production: ATDD in Legacy Code” (Sep 17 at 5:00 PM CET).
Don’t write broad integration tests to compensate for everything that unit tests can't do.
Broad integration tests are slow & fragile. They have similar limitations as E2E Tests, because both traverse across dependencies.
Unfortunately, I’ve seen many teams write broad integration tests!
Instead of writing broad integration tests, you need tests at different levels:
Narrow Integration Tests
Narrow Integration Tests are used to verify infrastructure logic (Driven Adapters). For example, the Order Microservice may have these Narrow Integration Tests:
Narrow Integration Test for the Order Repository - to verify database integration (e.g. using ORM, SQL, NoSQL, etc.), e.g. when we add entities, can we retrieve them (note that the test connects to a real database via Testcontainers)
Narrow Integration Test for the Event Published - to verify messaging queue integration (e.g. RabbitMQ, Kafka), that we are correctly mapping our events to the data structures required by the messaging brokers (note that the test connects to a real messaging queue via Testcontainers)
Narrow Integration Test for the Product Gateway - to verify integration with another microservice, e.g. with the Product Microservice - that we are correctly mapping our domain to the requests needed by Product Microservice, and whether we’re correctly interpreting the responses from the Product Microservice (note that the test connects to a stubbed version of the Product Microservice, not the real thing)
Narrow Integration Test for the Payment Gateway - to verify integration with an External System, e.g. PayPal - that we are correctly mapping our domain to the requests needed by PayPal, and whether we’re correctly interpreting the responses from the PayPal (note that the test connects to a stubbed version of the PayPal, not the real thing)
Narrow Integration Tests are also used to verify Presentation Logic (Driving Adapters). For example, suppose that Order Microservice exposes a REST API - then, we verify that we can correctly translate HTTP requests into Driving Port Requests, and that we can correctly map Driving Port Responses into HTTP Responses.
Component Tests
Component Tests are used to test each component in isolation from other Components.
For example, suppose that the Order Microservice depends on Product Microservice and depends on PayPal. In that case, we want to test a flow through the Order Microservice, starting at the Order Microservice REST API, including the real database (via Testcontainers) but excluding Component Dependencies (we stub out the Product Microservice) and excluding External System Dependencies (PayPal). In this way, the Order Microservice team can get feedback on whether Order Microservice itself works correctly.
Contract Tests
In the case of integrating with dependencies, since we use stubs, we want to be sure that the stubs match the real thing.
In the case of testing Order Microservice, we stub out the Product Microservice and we stub out PayPal. How do we know whether the stubs are actually valid, that their API matches the API of the real things? We use Contract Tests:
Contract Tests for the Product Microservice & Product Microservice Stub provide assurance that the Product Microservice Stub exposes the same API that the Order Microservice relies on when communicating with the real Product Microservice Test Instance
Contract Tests for PayPal & PayPal Stub provide assurance that the PayPal Stub exposes the same API that the Order Microservice relies on when communicating with the real PayPal Test Instance
Ultimately, we get assurance that the Real Dependency API and the Dependency Stub API match.
Acceptance Tests
Acceptance Tests provide us with assurance that business requirements are satisfied. They answer the question: Does the system work correctly (in isolation from external systems)?
For example, for an eShop System, we can test whether placing order works correctly, e.g. is the correct order price calculated. We do this by stubbing out ERP (an External System) to specify product pricing, so that we can verify, for some SKU & quantity, whether our eShop calculates the correct order total.
Summary
Testing at these levels helps you catch issues faster, keep tests maintainable, and have assurance you’re building the product right.
To learn more, read the Modern Test Pyramid.
Want to apply TDD in practice?
You tried TDD, but it didn’t work. That's why I'm going to help you practice TDD step-by-step. Apply TDD on a sandbox project. Access TDD in Legacy Code.
Sometimes developers complain that "we already have so many tests" - but maybe the real problem is not having tests at the right levels?