Types of software testing (in pictures)
In the book Growing Object-Oriented Software, Guided by Tests , we described the different types of tests that we use when designing software and showed how they fit well with the architectural style of Ports and Adapters by Alistair Cockburn .
In Ports and Adapters, the central place of the application is taken by the domain model, which has no points of contact with any parts of the infrastructure, whether it is a database, queue, UI, etc. But the model contains interfaces that define its relationship with the outside world in terms of the domain. Cockburn calls these interfaces ports. These interfaces are implemented in appropriate objects that interact with the outside world - Cockburn called them adapters. In distributed systems, different processes, each with its own domain model, interact with each other using ports and adapters.
In the diagram above, a large circle is a process, small (inside it) are objects. The domain is at the center of the process. Infrastructure modules (in the figure these are signed sectors), through which the process interacts with the outside world, are surrounded by a domain “circle”. Adapters that map domain concepts to technical implementations are located between them.
Below I will try to explain how the different levels of testing fit into Ports and Adapters.
Unit tests test individual objects, or their small groups within a single process. For example, during Test-Driven Development, we write unit tests, the results of which affect the tested code - we edit it when it does not pass any test cases.
The term "integration tests" can be applied to many types of testing. In our book, we used it to denote tests for some kind of abstraction from our code, which is implemented using third-party packages. Here we want to test that our implementation of abstraction integrates correctly with third-party code: to make sure that we have not made incorrect assumptions about how it works and just does not stumble on some unaccounted for errors. However, we cannot take and directly correct the errors found by these tests, because we do not have access to the tested (third-party) code.
Acceptance tests are user-oriented tests, they test the domain logic of the entire system, and demonstrate that it really works as expected.
Ports and Adapters allow you to run acceptance tests directly for the domain layer, because it is completely isolated from the technical infrastructure and the real world. Acceptance tests can interact with the model through port interfaces. Such tests will be damn fast. They are also easy to isolate from each other, because they do not save the state of the model (in the database or queue, for example).
Acceptance tests for a distributed system can initialize the domains of different processes in shared memory and refer to each other using implementations of their port interfaces, which will allow each specific test not to go beyond the boundaries of its process.
System tests, test the entire system by controlling it through open ports. They also test build, deployment, and system boot. Writing system tests in the early stages of development ensures that the system will always be ready for deployment as it develops.
However, such tests are wildly braking + the real conditions in which they are run (concurrency, asynchrony, the need to save data) greatly complicate the writing of readable tests isolated from each other.