I have used unit testing within TDD (Test Driven Development) for quite sometime, yet I did not really understand why we are doing it.
Everybody tells me it's the best practice, so I did it without hesitation.
I thought I was doing TDD although I wrote my test last, after the code was written.
After making my own research towards unit testing and test driven development, I felt enlighten.
Unit tests usually test on the scope of a class, module, or a component. Basically it test the smallest applicable unit on your software codebase.
Unit testing is not about about finding bugs, it is not merely a test at all!
The goal of making a unit tests is to help you design your software component.
You have to know in mind what your code are supposed to do before even start jabbing around on coding. Without it, you can't even start unit testing. It is often where we go implement a bunch of code without the right design in mind, we're just trying to figure it out along the way, therefore waste a great deal of time.
When you find your code difficult to test, it's a sign that you got yourself a smelly code:
Too much dependencies therefore you need to mock each one of those which is painful. It tells you that your code is too tightly coupled.
Unit test is way too long because your unit does multiple of things (low cohesive).
Common functionality is found among multiple different units violating DRY (low cohesive).
Unit testing also enables one to document cases in which a unit has been tested with. It also forces you think and probably find edge cases where a bug was found.
It can also be a regression test because simply changing your codebase has a potential to break your test, therefore having you to refactor the test and probably caught bug along the way.
Unit Testing Within TDD
Unit testing matches well with test driven development where forces you to write the simplest code that is necessary for the feature to work (test pass).
By doing this, you are avoiding unnecessary code, otherwise known as YAGNI (you ain't gonna need it).
A typical workflow would be a "red, green, refactor".
You write a unit test for the feature you are about to implement. Run the test and get a failed test (red).
Then you make the simplest code to make the test pass (green).
Afterward, refactor what's necessary.
Make it work, then make it right.
Selective Unit Testing and Techniques
Not all unit are meant to be unit tested. Some yields great benefit while some are just not worth the cost.
The cost of making unit testing are time needed to make the unit test and to maintain it.
If the ratio between cost and benefit is too high, it is better to avoid it.
Steven Anderson said that the benefit of unit testing is correlated with the non-obviousness of the code under the test.
In other words: abstraction level of the code.
If the code likely to have high abstraction level, a further design assistance generally needed.
If you can't figure out what the code was doing on a single glance, a further verification through unit testing is beneficial because it would be daunting to check all possible cases manually.
He also argue that the cost is correlated with the number of dependencies your code has, where more dependencies means more time needed to mock and adapt to future changes of your dependencies.
To sum it all up:
Complex code with few dependencies: cheap and highly beneficial to unit test.
Complex code with many dependencies: you will need need to refactor the code to two parts: a part which handles the complex logic and another which glues (interacts) with many dependencies. Unit test only the one that contains the complex code.
Trivial code with many dependencies: not worth to unit test.
Trivial code with few dependencies: not worth to unit test.
As for the complex code with many dependencies, the technique is to pull out the dependencies out and form an entirely new unit.
Use that new unit to interact with all of the dependencies needed and pass only what is needed within the dependencies to the unit that requires it.
What it means that when a unit expresses that it uses
dateClass.getTime(), it does not actually need the
dateClass, it needs only the time. So just pass
getTime() from the unit that glues all the dependencies to the unit that handles the complex logic.