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.

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply