The Testing Pyramid
The Testing Pyramid is a visual metaphor for the testing strategy that suggests having tests of different granularities and how many of each type should exist. The testing pyramid advocates for a balanced distribution of various types/layers of tests ensuring thorough coverage. The lower the layer, the more tests there should be, and vice versa. In other words, write lots of small and fast unit tests. Include some coarse-grained integration tests and have very few high-level tests that cover your application from end to end.
The concept of the testing pyramid was popularized by Mike Cohn in his book "Succeeding with Agile" introduced in 2009. According to him, the Testing Pyramid consists of:
Unit tests
Service/Integration tests
End-to-end tests
Unit Tests
Unit tests form the foundation of your test suite, ensuring that individual units of code work as intended. The definition of a "unit" varies; in functional languages, it’s often a single function, while in object-oriented languages, it can be a method or a class. Unit tests should outnumber other types of tests and run quickly.
There are two main types of unit tests: solitary tests, which mock all collaborators, and sociable, which use real collaborators except for those that are slow or have significant side effects. Mocks and stubs are common test doubles used to replace real objects with fake ones that provide controlled responses. This keeps tests fast by avoiding interactions with databases or external systems. Both solitary and sociable approaches have their pros and cons, and a mix of both can be used depending on the situation.
Effective unit tests should focus on observable behavior rather than internal implementation details. Good test structure for tests would be:
Set up the test data
Call your method under test
Assert that the expected results are returned
It is also known as the "given, when, then" structure and it is not limited to unit tests only. It helps keep tests clear and concise.
For more details check Unit Tests page.
Integration Tests
Integration tests are crucial for verifying how an application interacts with external components like databases, filesystems, or other services. Unlike unit tests, which isolate specific code sections, integration tests ensure that the various parts work together as intended.
There are different approaches to integration testing. Some prefer testing through the entire application stack, while others focus narrowly on individual integration points using test doubles (mocks or stubs) for external dependencies. This narrow approach, combined with contract testing, can make tests faster, more independent, and easier to manage. Integration tests often target the boundaries of a service, checking interactions with external systems. For instance, a database integration test would start a database, connect the application to it, perform operations, and verify the results. Similarly, testing integration with an external REST API might involve using tools like WireMock to simulate the external service, ensuring the application processes responses correctly.
Overall, integration tests provide confidence that an application can reliably interact with external systems, complementing unit tests to form a comprehensive testing strategy. They help ensure robust, reliable, and maintainable integrations, which are essential for modern software systems.
For more details check Integration Tests page.
End-to-End Tests
Testing your application via its user interface provides comprehensive end-to-end coverage, giving the highest confidence that your software works correctly. These tests automate browser actions to interact with your application and verify UI behavior, but they can be flaky and prone to false positives due to browser quirks and timing issues. In a microservices architecture, it’s challenging to determine responsibility for E2E tests as they span multiple services.
E2E tests require significant maintenance and can be slow, especially with numerous microservices. Therefore, it’s best to limit these tests to high-value user interactions and critical user journeys. Lower levels of the test pyramid should handle edge cases and integrations to avoid redundancy.
Tools like Selenium, combined with drivers for various browsers, are essential for E2E testing, but maintaining browser compatibility and versions across development and CI environments can be challenging. Using headless browsers like Chromium and Firefox in headless mode can simplify this process and avoid the need for a graphical user interface.
For REST APIs, subcutaneous tests can provide a less flaky alternative to full E2E tests by bypassing the UI layer. Libraries like REST-assured facilitate this by offering a DSL for HTTP requests and response evaluation, ensuring comprehensive API testing.
For more details check End-to-End Tests page.
Last updated