I guess many of us can relate to a situation depicted above.
A place where, unit testing is considered as a chore.
Something, that needs to be done as an addition to implementation as opposed to being vital part of the development process.
I think this mainly happens because unit testing demands a discipline to hold test code to the same standard as the production code.
Am I saying we should do TDD religiously?
Oh, no. No way!
TDD should be taken as an approach rather than as a rigid framework.
Try to write a small test first, even if it is just in plain English.
TDD forces us to think about what we really want from the unit; inputs, outputs, interface etc. resulting in much cleaner and organised codebase.
Also, remember tests do not need to to be perfect, neither does production code. Use TDD iteratively to get quick valuable feedback during development.
In code we trust!
A software application consists of many building blocks (we can call them units). We write the tests to prove these units work in isolation.
Once we made sure the they work, we can trust them!
How to identify a unit?
This is an easy one.
It’s entirely up to us!
There are no hard and fast rules for this.
We can consider a bunch of functions as one unit or test a single function as a separate unit. Can take a class and call it a unit or write tests for framework provided abstractions (Angular or React components).
So there is no rule, but there is pragmatism.
We can use a very simple rule:
If a unit proves difficult to test, then it’s very likely that it needs to be broken down into smaller components.
Unit testing encourages modularity.
It encourages practices that are even noted in the popular (and now at least 40 years old) Unix philosophy:
“Write programs that do one thing and do it well.”
“Write programs to work together.”
Test behaviour vs. testing implementation
Another interesting approach I’ve learned is:
This a great advice but can be confusing. It’s great for units that do not use dependencies or for end-to-end tests where testing implementation is a no no.
However, in many cases we can’t just blackbox our units.
This is because units interact.
Software components don’t usually exist in isolation.
In a real application many units are created to collaborate with each other.
So when we test a unit that was designed to use another unit as a dependency, we need make sure that the dependent unit is being used correctly.
This, often results in various stubbing and spying practices which are definitely the weakest and most debated parts of unit testing.
Travel the paths!
When the component under test has any kind of branching (if else, switch statements and so on), it is important to cover all the logical outcomes.
Okaaay, but what if the unit has way too many variable results?
Well, that means that the component could and should be broken down into smaller units until it’s a fully testable.
- Prove your code works so you and others can trust it.
- Hold test code to the same standard as the production code.
- Test all the paths (including the unhappy / erroneous paths)
- Hard to test? Break it down!
Part #2 — Let’s code!
Thank you for reading the first part of my “How to get started with Unit Testing?” series. I aim to publish the second part (covering realistic code examples!) in just a couple of weeks time.