Five Tips to Improve Your Unit Testing
After you got the hang of unit testing there is still so much space for improvement. In this post I want to share five tips with advanced testers I have seen to influence testing in the right direction.
Do you have additional tips and tricks to improve testing? Please leave a comment!
1. Be Pragmatic About a "Unit"
"A unit is a class" or even "a unit is a single method" are two dogmata people use to explain unit testing. This for good reasons: Following a straight line helps to grasp the concept when getting started, recognizing the code smells discovered through testing issues on that basis is very important and aiming for coherent classes and pure methods/functions is always a good idea.
But as you have practiced unit testing on basis of these dogmas you will notice that in some cases pragmatism beats the dogma. That is perfectly valid. There can be many reasons to test a bunch of classes together instead of focussing on just a single one:
A class uses multiple rather trivial other classes where it is pure overhead of mocking these (e.g. DTOs with very few logic).
You are in the phase of refactoring and don't know if the result turns out exactly to be what you want.
A class dispatches logic to some other classes to make the code re-usable but the logic is only complete in combination.
The individual classes are rather simple, but them playing together results in an algorithm that's worth testing.
But beware: Never use an external system (e.g. database, hard disk, web-service) in a unit test.
2. Test Where the Logic is
I'm not a fan of CodeCoverage. But when you just get started with unit testing it is a great tool to reflect your tests and, even more important, the parts of your code which are not touched by tests. But following Code Coverage after this initial learning phase often drives people into testing trivials like getters/setters, constructors and so on.
Please do not test these trivials explicitely. It's a nice practice for your first few unit tests, sure. And cases might where you feel the need to test a setter (e.g. if it contains rather complex validation). In all other cases: Trust yourself that you will use the trivial code in other test cases, for examle such from tip 1 or 5.
Instead of focussing on trivials, look where there really is logic. In technical terms: Where are the loops, conditions, private methods and so on? Focus on these places. Accept the challenges they offer and write tests for stuff that matters.
3. Continuously Refactor Test Code
As your system grows your test code will (hopefully) grow, too. Some people insist that changing tests is a no-go because there no guarantee to not break a test. I disagree: As within your production code you will find better arrangement for your code and will get a deeper understanding on how to realize certain requirements over time. You will produce duplication and (hopefully!) become aware of it. You will implement hacks to achieve results fast and clean them up when you find better solutions over time.
It is important to reflect over your test code in a very similar way than you do it with the productive code. However, there are two important hints to follow:
a) Never change production and test code at the same time. When you refactor your test code, the production code is the reference to asserts tests are still working as they should. And of course your tests are the assertion while working on your production code.
b) Keep test code simple. Simple does not mean dirty, it means easy to read and understand. Your goal should neither be to reduce the ammount of test code to a bare minimum nor to find the highest degree of automation. Tip number 4 will go a little bit more into detail here.
4. Build Your Own Set of Utilities
PHPUnit (and other unit test frameworks) ship with a large set of generic tools and utilities. Ranging from assertions over data generators to mock frameworks - you have a large selection inside of frameworks and extensions to download. While these can give you a good technical basis for your tests, none of them provides the golden hammer to suite all your needs.
You will start writing utility methods for setting up certain fixtures (e.g. a
Product) and implement custom assertions (e.g.
assertUniqueProductSet()) based on your data structures and code patterns. That is a good thing! Identify the patterns that evolve in your test suite and reduce duplication by extracting utilities.
Tool methods can typically reside in common base classes (due to PHPUnits inheritance scheme) or traits.
5. Always Write Tests for Bugs
You can discuss a lot about worthwhile tests and opinions on this topic vary widely. But there is one category that always makes sense: Regression tests for bugs. Whenever you encounter a bug in your software, write a test that fails due to this bug and fix the bug afterwards.
If someone discovers a bug in your software that means that the code is actually in use, so it's important for your users and therefore deserves a test. The pure existence of the bug shows that there is room for another test. And, you will need to reproduce the bug anyway. Doing this by hand is the same effort that doing it through a test case. So the latter version will safe you time and gain you a test where there would have been only a bug fix before.