Much like the Hitchhiker’s Guide to the Galaxy, Wikipedia has many omissions and contains much that is apocryphal, or at least wildly inaccurate, it does contain this nugget of truthful information:
Unit tests are typically automated tests written and run by software developers to ensure that a section of an application (known as the "unit") meets its design and behaves as intended. In procedural programming, a unit could be an entire module, but it is more commonly an individual function or procedure. In object-oriented programming, a unit is often an entire interface, such as a class, but could be an individual method. By writing tests first for the smallest testable units, then the compound behaviors between those, one can build up comprehensive tests for complex applications.
It further goes on, after a bit of a long-winded detour, to say the following:
The goal of unit testing is to isolate each part of the program and show that the individual parts are correct. A unit test provides a strict, written contract that the piece of code must satisfy
As developers, we write unit tests for ourselves first and the business second. That requires a mental shift because, well, who wants to write tests? We only write them because the business hired a consultant who said we need test automation to accelerate delivery right?
What if I told you that by writing tests now, you won’t have to come back to this code to fix bugs (and yes, there will be bugs) a couple of sprints later? What if by writing tests now, during implementation, you can carry on to the next new thing? Doesn’t that sound good? You get to carry on solving problems, making new things knowing that what you made before is solid. You’re not just a programmer, you are skilled in your craft and it shows.
Tests can help shape your design; if you can’t easily write a test that consumes your new code, there’s a good chance that others will have a hard time consuming it as well. Integration becomes difficult, adhoc adapters appear as if summoned from the depths of the fragile codebases of the 90’s, the system becomes brittle and resistant to change.
Good tests prevent regressions – unintentional behavioral changes – that creep in during refactorings or when adapting old code for new features. A good test suite that’s run during the continual integration (CI) process ensures that breaking changes never make it to production.
One last point to remember: Unit tests are not integration tests nor are they acceptance tests. These types of tests exist at a higher level in the testing pyramid.
There are times that having to write tests feels overwhelming. Imagine staring at a class with many private methods – where do you even start?! As it turns out, it’s not nearly as hard as it appears. There are a few principles that, if followed, can help you write great tests:
- Only write tests for public (or internal) methods. If you can’t get to code through a non-private entry point, it’s unreachable and should most likely be deleted. YAGNI
- A unit test should have no external dependencies. Seriously.
- No web api calls
- No external files
- NO DATABASES!
- No ‘deployment items’
- Do not rely on external state
- A unit test should execute as fast as possible. Modern IDEs can run your unit tests in reaction to changes you make to the code (e.g., VS 2019 Live Unit Testing) and flag failing lines right in the editor.
- Mock your dependencies that have interfaces
- Fake your calls that can’t be mocked
- Don’t just test the happy path – that’s not really helpful
- Do test your boundary conditions and expected failures. You don’t need to test all possible input values; only test those that make sense to test.
- Do use code coverage tools to know when you’re done.
- Unit Testing For Modern Developers Part II: Using Fakes
- Unit Testing For Modern Developers Part III: Using Mocks
- Unit Testing for Modern Developers Part IV: Test Driven Development