Over the last few years, we have been adding unit tests to our existing product to improve its internal quality. During this period, we always had the challenge of choosing unit vs. integration tests. I would like to mention some of the approaches we have applied to improve the quality of our existing system.
At its core, unit testing is about testing a single component at a time by isolating its dependencies. The classical unit tests have these properties: "Fast, Independent, Repeatable, Self-Validating, Timely". Typically in Java, a method is considered a unit. So, the traditional (and most common) approach is to test the single method of a class separated from all its dependencies.
Interestingly, there is no straight definition of what makes a unit. Many times a combination of methods which spread across multiple classes can form a single behavior. So, in this context, the behavior could be considered as a unit. I have seen people breaking these units and writing multiple tests for the sake of testing a single method. If the intermediate results are not significant, this will only increase the complexity of the system. The best way to test a system is to test with its dependencies wherever we can accommodate them. Hence, we should try to use the actual implementation and not mocks. Uncle Bob puts this point very well, "Mock across architecturally significant boundaries, but not within those boundaries..." in this article.
If the software is built using a TDD approach, it might not be a challenge to isolate dependencies or add a test for your next feature. But, not all software is built like this. Unfortunately, we have systems where there are only a few or no tests written. When working with these systems, we can make use of the above principle and use tests at different levels. Terry Yin provides an excellent graphic (which is shown below) in his presentation titled Misconceptions of unit testing. This shows how different tests can add values and what the drawbacks are.
Many of our projects use Java and the Spring framework. We have used springs @RunWith and SpringJUnit4ClassRunner to create AppLevel Tests which gives you the objects with all its dependencies initiated. You could selectively mock certain dependencies if you would like to isolate them. This sets a nice platform to write unit tests with multiple collaborating objects. We call them App level tests. These are still fast running tests with no external dependencies. A different term was chosen to differentiate this from the classical unit test. We also had Integration tests which would connect with external systems. So, the overall picture of developer tests can be summarized as below:
|Tests||Naming convention||Runs at||When to use||Exec Time|
|Unit Test||Ends with Test||Every build||Rule based implementations where the logic can be tested in isolation||Few Milliseconds|
|App Level Tests||Ends with TestApp||Every build / Nightly builds (Teams choice)||Tests the service layers in connection with others. Frees you from creation of mock objects. Application context is loaded in the tests.||Few Seconds|
|Integration Test||Ends with TestIntg||Runs on demand when a special profile is used in build.||All the above + Use when you need to connect to external points like DB, web services etc..||Depends on the integration points.|
|Manually Running Tests||Ends withTestIntgManual||Manually running tests, Used debugging a specific problem locally||All the above - Can't be automated.||Depends on the integration points.|
This approach gives the developers the ability to choose the right level of abstractions to test and helps in optimizing their time. Nowadays, my default choice is App Level tests and I go to unit tests if I have a complicated logic to implement.