In my previous two blog posts I talked a lot about unit tests and how to write valuable unit tests. But what about integration tests? When should you write these, and how do you write valuable integration tests? I’ll try and answer these questions in this blog post.
Integration tests operate on a higher level of abstraction than unit tests. The main difference between integration and unit testing is that integration tests actually affect external dependencies. Integration tests interact with external dependencies and are therefore much slower than unit tests. Integration tests are needed to test the application services layer, which interacts with external systems. You may have direct control over some of these external systems, such as a local database, or they may be third party systems over which you have no direct control.
In my previous blog post I discussed the Collaboration Verification style of unit testing and how it is not a good style for unit testing. When it comes to integration testing, however, collaboration testing comes into its own. Integration testing is all about testing collaboration between applications and external systems.
So what is the best way to write integration tests? Vladimir Khorikov argues there is little value in writing integration tests that do not deal with the external dependencies directly. If you mock out the external dependencies such as databases, then all you are really testing is boilerplate code, and so the chance of catching regression errors is small. The use of mocks also increases the maintenance cost of the integration test. These two issues mean this type of integration testing is not valuable and usually not worth doing.
If you work directly with the external dependency (such as a database) in your integration tests, the tests will be slower but will be much more valuable overall. They have a high chance of catching regression errors and a low maintenance cost, and tend to produce few false positives.
When writing integration tests you need to consider the type of external dependencies you are dealing with. If you have full control over the dependency, for example, an internal database or file system, you should test that dependency directly in the integration tests. If you do not have control over the dependency, for example, a third party web service or customer system, you will most likely need to substitute them with test doubles (such as mocks). However, you may be able to test it directly if the external system is stable enough .
For further information on the avoidance of mocks see Khorikov’s blog post http://enterprisecraftsmanship.com/2016/11/15/when-to-include-external-systems-into-testing-scope/
Some general rules to follow when writing integration tests:
- verify collaborations at the very edges of your system
- only use mocks if you don’t have direct control over the external system—once you use mocks your tests become less valuable
- use the same type and versions of the database In your integration tests as in production
- to isolate integration tests from each other, run them sequentially and remove data left after test execution
- the best way to do test data cleanup when testing the database is to wipe out all test data before test execution—each test will then create the data it needs
- each developer should have their own version of the database; i.e. their own instance—the DB schema and reference data should be in a version control system
I advocate this practical approach to automated unit and integration tests:
- employ unit testing to verify all possible cases in your domain model
- with integration tests, check only a single happy path per application service method—if there are any edge cases that cannot be covered with unit tests, check them as well
Note: this approach to integration testing, where we use the actual external dependencies where possible, exercises a large amount of code and reduces the need to write full end-to-end automated test cases. I plan to return to this topic in a future blog post.
Over my last three blog posts I’ve written a lot about the best way to tackle the task of writing automated unit and integration tests. There are three key concepts that I would like you to take away:
- Only write valuable unit and integration tests. By doing this you will reduce the overall number of unit and integration tests that need to be written and maintained.
- Design the code so that the tests will be maintainable and valuable. Writing tests and good code design go hand in hand.
- For integration tests, only use mocks when you do not have full control over the external system.