This article was done using my notes from Paweł Czyżewski (2023, Coding testing strategies) private arcticle.
“Every kind of strategy is focused on different goals and requires paying different price for having them.”.
Thoses tests focuse on checking single component (instance of class, structure) expected behavior when all external dependencies (of this class) are provided in form of controlled mock.
That allows for high coverage, ideally all possible executions paths within that component: starting from happy paths, handling invalid data, and finally handling exceptional cases.
This is the best moment for assuring that component is handling unexpected situation in expected way. For example, if we test an repository implementation for mysql, we can simulate mysql client throwing a network exception while being in the middle of multistep transaction. We can expect that rollback is being called, specific error message is sent to error logger, with certain error level and specific exception is returned by repository method.
There can be several perspectives on integration tests.
We can test if two, or more components from the cosebase are cooperating in expected way. Mainly, we want to verify that our codebase interacts as expected with external resources. For example, we can test if our MySQL repository is able to interact with real MySQL server instance. If data (most of the time in form of our DTO / Aggregate) that we are providing through repository interface is actually reflected on MySQL server. This test requires data setup (if applicable), actual data check using native SQL queries.
We can test all components of our code that put together can make create a Use case. This is edge to edge components integration. Here we setup test for concrete use case by taking components representing, for example api access layer, application layer that manages core code usage, and other infrastructure layer, like data store, messaging, logging and so on. Same as in previous test we prepare test data and do actual checks by using native resource clients.
This kind of test is most complex one (in terms of setup) and closest to actual usage in real life. Here we want to start and test our application that is ready to handle multiple, different use cases at the same time. If we talk about application that is providing RESTFUL api access then test would be about using that API only to, for example create and manipulate product data through it’s whole life cycle. If our application is interacting with other applications that we have no control, or knowledge of then we can use such applications mocks. For example we can prepare mock, fake service that exposes api which follows official documentation, but is returning static data controlled by us. Same goes with testing public events produced by our application. We can setup a fake consumer that only purpose is to compare received data against expected sets.
UTs are easiest and lightest to run as they are using only core language capabilities and no other resources like database are required. We have best control over execution path and we are able to verify handling any edge case. We can also test how our components behaves with number of diffrent data sets. UTs are enablers for 100% code test coverage and definitely fasters to run.
The downside is that mocking all dependencies, to provide number of behaviours is prone to a lot of code. Testing code will be always greater than tested one. Sometimes component is working with complicated data structure and multiple DTOs that can make data preparation difficult. This could mean that our component is too complicated and we should refactor it (break into smaller one) or try to test with other real component that can provide data in more easy way.
ITs are slightly more complex to setup as they mostly require bootstrapping some data in data store, messaging system. Use them to verify it you can communicate with real, external resource in expected way. These tests requires starting real services before running them. Any test that creates or manipulates data has to make sure it creates unique data and does clean up after tests is done (no matter if test failed or passed) so there is no chance that one test influence (or requires) other test to pass. ITs are also giving good opportunity to check application behaviour in certain use case with number of features switch / configuration variables.
FTs are the heavies one and require more work to prepare test environment. FTs doesn’t always require data preparation or clean up as they should be running in dedicated, isolated environment. There can be, and usually there are dependencies between tests as one of test case might be doing some data setup (for example registering client account, creating product for sale). We can run same FTs with different environment setup that reflects actual configuration in staging, production, etc.