Visualising Test Terminology
In our book, Growing Object-Oriented Software, Guided by Tests, we describe the different kinds of tests we use to drive the design of a computer system and show how they guide the evolution of the system's structure towards the Ports and Adapters architecture.
The Ports and Adapters architecture makes the application domain model central to the system and isolates its definition from the program's technical infrastructure, such as databases, message queues and user interface toolkits. The application domain model includes interfaces that define its relationships with the outside world in terms of application domain concepts (Cockburn calls these interfaces "ports"). These interfaces are implemented by objects that translate the application domain concepts onto an appropriate technical implementation (Cockburn calls these objects "adapters"). In a distributed system, each process has an application domain model that communicates to other process via ports and adapters.
In the diagram above, the big circle with a drop-shadow represents a process; small circles represent objects. The application domain model is at the center of the process. Technical modules through which the process interacts with the outside world are at the outside of the process. Adapter modules that map application domain concepts onto technical implementations are between the two.
Below I've tried to portray how the different test types we describe in the book relate to the Ports and Adapters architecture.
Unit Tests
Unit tests exercise individual objects or value types or small clusters of objects within in a single process. In particular, when doing Test-Driven Development, unit tests exercise code that we can change in response to the feedback we get from writing the tests.
Integration Tests
The term "integration test" can apply to many different kinds of test. In the book, we use the term specifically to mean the test of an abstraction that we own but have implemented with some third-party package. We want to test that our code implementing the abstraction integrates with that third-party package correctly: that we have not made any incorrect assumptions about how it works or tripped over any bugs that we will have to work around. However, we cannot respond to feedback the tests give us about the internal quality of that third-party package because we cannot change it.
Acceptance Tests
Acceptance tests are customer-facing tests that capture the domain logic the system must perform and demonstrates that it performs them.
The Ports & Adapters architecture makes it possible to run acceptance-tests directly against the application domain model because the domain model is cleanly decoupled from the technical infrastructure that connects it to the outside world. Acceptance tests can interact with the domain model through its port interfaces. Acceptance tests written against an isolated domain model can run extremely fast. Because there is no persistent state involved, in databases or message queues for example, it is easy to isolate tests from one another.
Acceptance tests of a distributed system can instantiate the domain models of different processes in the same memory space and link them by implementations of their port interfaces that do not go out-of-process.
System Tests
System tests exercise the entire system end-to-end, driving the system through its published remote interfaces and user interface. They also exercise the packaging, deployment and startup of the system. Writing system tests first addresses integration concerns early in the development process and ensures that the system is always ready for deployment to users as development progresses.
However, system tests run significantly slower than in-process tests and the system's natural concurrency, asynchrony and persistent state can make it difficult to write reliable tests and isolate tests from one another. I've documented some techniques for testing asynchronous systems previously.