How To Write Good Unit Tests
How To Write Good Unit Tests
What makes a good unit test?
Meaningful Naming
Are these helpful unit test names?
Test_6
Test_Something
Accounts_Test
No, they are not. These names are devoid of meaning. Readers ought to be able to comprehend the purpose of a unit test immediately;
A technique that I like is using Given/When/Then to name tests. Here are a couple of examples:
Given_No_Matching_UseCase_When_Try_GetUseCase_Then_Throw_UnknownUseCase
Given_No_AccountGroupName_When_Call_BuildQuery_Then_Throw_ArgumentNullException
But it doesn’t have to be Given/When/Then. For example, in the Bowling Game Kata, Roll_Guttergame
is an excellent contextual unit test name for a rolling all zeros.
The reader should immediately get the idea of what a test is all about. There should be no head-scratching.
Easy to Understand
I have seen unit tests that were two pages long. There were many lines of setup and verification all on display. With tests like that, it’s difficult to discern which parts are relevant to a particular test and which are just mechanics.
When we have to brace ourselves before we dig into a unit test to try and understand what is going on, that is not a useful unit test. It should not be like we are trying to decipher the Dead Sea Scrolls.
What happens to tests that are hard to understand and maintain? When they start failing, they will get commented out or deleted.
The body of a unit test should be short and understandable. We must use all our programming prowess to make unit tests intuitive to understand.
Following is an example where I did not fully achieve this:
[Theory] [InlineData(9876.5432, 9876.54)] [InlineData(5432.9876, 5432.99)] public async Task Given_4DP_Budget_When_Call_UpdateBudget_Then_Save_Budget_As_Rounded_To_2DP( decimal budget4DP, decimal roundedBudget2DP) { var useCase = SetupUseCase(); await useCase.UpdateBudget(new BudgetChange(AccountGroupName, budget4DP)); VerifySaveNewBudget(useCase, roundedBudget2DP); }
As the name indicates, the test ensures that the UpdateBudget()
method rounds incoming 4 decimal place budget figures to 2 decimal places.
The test has only three lines, which is nice and short. However, when we read the test code, there is at least one aspect that is a little bit off. The test code exposes an unnecessary detail which may cause minor confusion. Do you know what I mean? Maybe reread the test.
The test uses AccountGroupName
. Why is this here? How is it involved in budget rounding? It’s not. AccountGroupName
is needed to initialise BudgetChange
. It’s a detail that is detracting from the meat of the unit test.
What should we do? Easy. Hide it:
[Theory] [InlineData(9876.5432, 9876.54)] [InlineData(5432.9876, 5432.99)] public async Task Given_4DP_Budget_When_Call_UpdateBudget_Then_Save_Budget_As_Rounded_To_2DP( decimal budget4DP, decimal roundedBudget2DP) { var useCase = SetupUseCase(); await useCase.UpdateBudget(SetupBudgetChange(budget4DP)); VerifySaveNewBudget(useCase, roundedBudget2DP); }
We have relegated the instantiation of class BudgetChange
to method SetupBudgetChange()
. We have thus hidden AccountGroupName
from the high-level test code.
So, the only moving parts the test is exposing are the ones that we are interested in:
- the useCase instance that does the work,
- the incoming 4DP budget figure, and
- the rounded 2DP budget.
All other detail still exists but has been conveniently hidden away in helper methods.
Leave a Reply
Want to join the discussion?Feel free to contribute!