Benefits of unit testing

I find it surprisingly hard to get new developers to really engage with unit testing but then when I think back to when I had just graduated and what I thought about unit testing:

I'm here to write code, not to test!

I shouldn't be surprised.

Unit testing works slightly counter intuitively - by undertaking additional work (writing unit tests) we can actually save having to perform additional work (fixing bugs) and so speed up overall development. This speed increase is only one benefit, below I want to go through the other benefits that we get by embracing unit testing:

Benefits

  1. Discover bugs quickly
  2. Match the specification
  3. Easier refactoring
  4. Better integration
  5. Living documentation
  6. Better design
  7. Easier maintenance
  8. Save time
  9. Provide good feedback
  10. Prevent feature creep
  11. Avoid embarrassment
Discover bugs quickly

A unit test should be written around the same time as the method being tested is written. By doing this we ensure that our understanding of the functionality expected of the method is fresh and at the forefront of our minds. This means that if any unit test fails, it should be relatively straightforward to determine why its failed and rectify it.

Match the specification

When we write unit tests we should be checking that the method satisfies all the requirements defined in the specification rather than writing it based on the method's implementation. As such when we can avoid shipping our apps stating that it does something that it turns out it can’t.

Easier refactoring

Often the main argument for introducing unit testing is to help find bugs and while that is a very important part, the main strength of unit testing is the confidence it gives us to alter existing classes and method without introducing a regression. Without unit testing refactoring an existing class or method can be a very time consuming and difficult process. As after refactoring a method you would then need to manually check that the change did not result in a bug being introduced into another part of your app that was previously working. Usually this would be checked through exercising the UI - this is more time consuming than running a test suite and more prone to error. By using unit tests we can ensure that the class or method that we changed still works from the outside (black-box) the way that it did before we refactored.

Better integration

By unit testing our app we can have confidence that the individual classes and methods in our app work in isolation, when we then combine these individual parts together we should have fewer issues than without unit tests and be able to more quickly identify these issues

Living documentation

A suite of unit tests allow for a living document to be created that helps new developers to the project gain an insight into how the app behaviour. Unit tests detail not only the “happy paths” through the app but also the “unhappy paths”. As the unit is tightly coupled to the app, it is more likely to be maintained and more accurate than the official documentation which is often out of date.

Better design

In order to get the most out of unit testing, your project has to organised into small, independent tasks that can each be tested in isolation. small, independent and isolation are often phrases that used to describe well designed code. Lets look a simple before and after example:

func formatDownloadStatus() -> String {
    return ("Download Status: \(Downloader.shared.downloadStatus)")
}

The above method is responsible for accessing the downloadStatus property on the Downloader singleton (don't worry you haven't seen Downloader's implementation - downloadStatus is just a String instance holding values such as Queued, Downloading, Completed, etc) and combining it with a constant String value. This combined String instance is then returned as the output of this method.

A common way to make your code unit testable is to ensure that all dependencies that your code has are explicitly defined:

func formatDownloadStatus(for downloadStatus: String) -> String {
    return ("Download Status: \(downloadStatus)")
}

In the above method, rather than accessing downloadStatus via the Downloader singleton, it is passed into the method. This not only makes it super simple to unit test as we don't need to figure out how to mock out the Downloader singleton but also leads to a more decoupled codebase as this method no longer needs to even know that Downloader exists.

You often find that if a method is difficult to write a unit test for, it is an indication its design can be improved. A key consideration when writing software for any platform is: can I abstract this method/class out and reuse it another settings? If your answer is no it may be worthwhile to spend the time removing whatever is preventing, so that you produce code that is loosely coupled.

Easier Maintenance

Unit tests give us a suite of regression tests that we can run whenever we make any changes to the app. This is especially useful if the original developers are no longer involved. If we find that a bug got out into the “wild” (production) then all we need to do is create a new failing unit test that tests the method against the expected behaviour and then refactor the failing method until that unit test passes. This unit test will then ensure that going forward we should never regress and introduce that bug again.

Save time

A unit can be ran at the speed of your CPU. This is of course much more quicker than a manual tester could run the same tests. Depending on the size of the test suite you should be able to run it multiple times a day giving you very quick feedback as to whether your changes are breaking existing behaviour. Now this is not to say that you don’t need to still have manual testers but unit tests should mean that the quality of app that the manual testers test against should be higher than if you didn’t have any unit tests.

Provide good feedback

Feedback comes in two forms:

  • during the writing of the unit test
  • during the running of the unit test

We have already spoken about the writing feedback in Better design so this section covers the other type of feedback. When a unit test fails you know exactly which one failed, what the system pre-conditions where before the test was executed and are able to reproduce the failure by re-running the unit test. This feedback should allow you to be able to more easily identify the cause of bug and get on with fixing it.

Prevent feature creep

Unit tests help to prevent you from introducing unnecessary features into your app because every new feature added needs to be fully unit tested. All of this testing of unnecessary features takes time and if there is anything us developers hate doing it's unnecessary work.

Avoid embarrassment

No one likes to be told that something that they have helped created does not work. By introducing unit tests (provided they are good) you should greatly reduce the chances of this happening and avoid any embarrassing conversations with your boss.

What do you think? Let me know by getting in touch on Twitter - @wibosco