Dependencies in Unit Testing
Complete Developer Podcast - A podcast by BJ Burns and Will Gant - Giovedì
Unit tests are a great way to make sure that your code is more stable. Not only do they let you check your assumptions when you make changes, but they also tend to force you to structure your code in a way that keeps it cleaner. As you get more test coverage, this pays off by making it easier to do larger reorganization of your code with less fear of breakage. The cleaner structure can also make it easier to diagnose problems in the code without doing a bunch of manual tracing. Finally, unit tests can serve as a bit of “living documentation” for the expectations around your code, which can often make it easier for newer developers to pick it up. However, the previous advantages are something you will only enjoy so long as your code doesn’t interact with a file system, database, network, third party app, or physical hardware. Each of these situations can introduce intermittent errors, limits to the number of calls you can execute, interference from state changes, and they all will generally slow down your tests. As you get more tests, these disadvantages pile up and make it less likely that unit tests will be run, because they become time consuming and unreliable. Fortunately, because we are talking about unit tests, it’s expected that you are only testing how your code reacts to different conditions, rather than testing underlying conditions that may not be reliable. This means that you can effectively “fake” a dependency in such a way that your tests only touch the code that you are trying to test. Moreover, if you do it right, this approach can also make your tests faster, which makes your team more likely to run them in the first place. Managing dependencies in unit tests is something you pick up over time. Like most other code, your approach to dependencies in a unit test scenario should start with something simple and only add complexity when it is valuable and necessary. Episode Breakdown Working with Dependencies in Unit Testing Unit tests versus other types of tests It’s probably necessary at the start to explain the difference between unit tests and other test types, simply because a lot of so-called “unit tests” are anything but. A “unit test” tests a single, logical unit of code, not including its dependencies. It’s intended to be fast, to only have its results change based upon the way that the code works, and to make it easy to determine where and why a failure occured. An integration test tests the way that multiple pieces of code work together. It’s intended to be a bit slower, more vulnerable to latency, and most importantly, to test how several pieces of code work together. It may be harder to troubleshoot than a unit test and it WILL almost certainly run slower. A performance test tests how a system responds to load, how quickly a process can run, and how resources are used by the process. Generally, this is done over several components, although sometimes you will see single-unit performance benchmarking in the wild. Functional Tests focus on the business requirements of the application being tested. They only check final outputs, rather than intermediate steps. End-to-End tests attempt to replicate the behavior of a user on a system and make sure that longer workflows work properly. Why dependencies make things difficult. Dependencies introduce latency. Making calls to real dependencies will slow down your unit tests, sometimes drastically. Dependencies introduce breaking changes that aren’t relevant to your tests. Dependencies may change under the hood, causing your test to fail, even if the actual code being tested is not the issue. While it’s helpful to catch this, it’s annoying to troubleshoot. Dependencies can introduce intermittent errors to your code.