Community: A Tester's Guide to Unit Testing

A Tester's Guide to Unit Testing

By Craig Risi on February 07, 2019

Testing has changed from a discipline that was largely manual and isolated from the rest of the development, to one that is highly technical, focuses on automation and requires closer collaboration with testers. Sadly, I have found that this change has not been implemented in many South African tech companies. This article will explain why it is important to understand and effectively implement unit testing, as well as provide tips for writing unit tests that ensure both testers and developers deliver higher quality code.

Craig_Inner-article-Image

When I first made the transition from software developer to tester, the big trend was to keep testers and developers separate to ensure the independence of the testing team. This concept of "not marking your own homework" was seen as a strong purveyor of quality, but over time many companies have realised that this actually created more problems than it fixed. For instance, as a tester, I would end up focusing on how to find faults in the software, rather than how to build it better. It also meant that my goal as a tester was not aligned with the developers' which naturally meant that I would work separately from them.

These days I've learnt that the best way to build high-quality software is to get it right at the design level. The best way to do this is by designing your software in a manner that suits your testing strategy and allows for small, easy-to-maintain components that allow for decent unit testing. It is also important to test as close to the code as possible by playing in the unit test space, because this is where the majority of testing efforts need to be focused.

This is why I will spend some time discussing how to get the design of your unit tests right, as well as how to think about testing.

Why should testers focus on a unit test?

As a tester, one of the first mindset changes that I required for unit testing was to shift focus from the big picture integration of the software, to the details of the code. By doing this, I saw how systems can be better mocked and isolated to allow the unit test to be effective.

If you are a tester and have not made the transition to intensive unit testing, this might be because you are not yet convinced of its benefits over-and-above the regular high-integration test coverage. Perhaps knowing the some of the benefits will help get you there:

Collaboration

The most important benefit is that it will force collaboration between developers and testers. So, if you're a tester, and you wish that you could get more involved in how the software is designed, then getting involved with unit testing is the best place to start.

Ever since I've been involved with the unit tests, both the developers and I work closer together on the quality of the product - which is always a good thing!

It also ensures that the development team hits the required test coverage needed to reduce the integration testing that can be done elsewhere. This is because the code often works the same whether it is tested at a unit level or integration level, so you can reduce effort later by testing it earlier.

Test driven development (TDD)

TDD is a big buzz word that calls for the tests relating to a developer's code to be clearly identified and written before they've finished their development work. This makes it easy for them to know what their code is supposed to do and the parameters it needs to pass in order to be deemed good enough. Unit tests help with TDD by making it easy to associate lines of code with tests and thereby identify exactly which parts of the code are not behaving correctly.

This helps developers when they want to submit their code and know that it's met all of the relevant criteria almost immediately. It also forces developers to think more clearly about what they're designing, as the guidelines and criteria to pass are clearly understood.

Finding bugs early

Not only are unit tests faster to execute, but it also means that defects are found the moment code is pushed into the pipeline. This means that the developer can see the failure straight away and get it fixed before the code even makes into any form of integrated test environment. It's like I'm not just automating the test, but automating the whole defect life-cycle too.

Reducing costs and duplication

Faster execution and early error detection are going to save efforts later in the development cycle, making it cheaper for everyone. This reduces the duplication of covering the same scenarios at an integration level that developers cover in their unit tests.

Documenting the code and increasing traceability

Another benefit, which should appeal nicely to testers, is that unit tests make it easy to identify what the code does while allowing for traceability – which is vital to ensuring software quality.

Most testing repos will allow software requirements to link up to unit tests. This helps testers track coverage a lot easier without them needing to do all of this themselves.

Improved ideation

One of the things that I have found with testing is that not everyone does a good job at it. Not all developers have the knack for thinking of good test scenarios, so the quality of code and tests can vary greatly. By getting involved in unit tests and focusing on isolated use cases, I was able to apply my testing mindset to identify better test scenarios. This meant that I could cover more tests at a unit test level, but also that there was less to do later on. Thus, eliminating the most horrible testing crunches that are often part of any sprint cycle.

What should be unit tested?

Testers typically like to test everything when the code arrives in their environments. However, when there is confidence that the code is correctly tested at a unit level, the tester can focus on high-priority and complex scenarios instead. Before jumping into unit tests, it is also helpful to understand what scenarios are best-suited for unit testing. While you want most of your testing to be done in this way, there is still a need for integration and end-to-end tests that ensure the individual parts all behave correctly and in unison.

Entry and exit points: All code receives an input and then provides an output. Essentially, what you are looking to unit test is everything that a piece of code can receive, and then you must ensure it sends out the correct output. By catching everything that flows through each piece of code in the system, you greatly reduce the amount of failures that are likely to occur when they are integrated as a whole.

Isolated functionality: While most code will operate on an integrated level, there are many functions that will handle all computation internally. These can be unit tested exclusively and teams should aim to hit 100% unit test coverage on these pieces of code. I have mostly come across isolated functions when working in microservice architecture where authentication or calculator functions have no dependencies. This means that they can be completely unit tested with no need for additional integration.

Boundary value validations: Code behaves the same when it receives valid or invalid arguments, regardless of whether it is entered from a UI, some nitrated API, or directly through the code. There is no need for testers to go through exhaustive scenarios when much of this can be covered in unit tests.

Clear data permutations: When the data inputs and outputs are clear, it makes that code or component an ideal candidate for a unit test. If you're dealing with complex data permutations, then it is best to tackle these at an integration level. The reason for this is that complex data is often difficult to mock, timeous to process and will slow down your coding pipeline.

Security and performance: While the majority of load, performance and security testing happens at an integration level, these can also be tested at a unit level. Each piece of code should be able to handle an invalid authentication, redirection or SQL/code injection and transmit code efficiently. Unit tests can be created to validate against these. After all, a system's security and performance is only as effective as its weakest part, so ensuring there are no weak parts is a good place to start.

Writing good unit tests

Now you still need to learn the skills needed to write unit tests correctly. For the sake of both developers and testers, these guidelines will help you ensure that your unit tests are written effectively, and in a manner that testers can contribute to easily.

Characteristics of a good unit test

To determine whether a certain scenario would be suited for unit testing, use the following characteristics to help you:

  • Fast: It is not uncommon for mature projects to have thousands of unit tests. Unit tests should take very little time to run - milliseconds even.
  • Isolated: Unit tests are standalone, can be run in isolation, and have no dependencies on any outside factors such as a file system or database.
  • Repeatable: A unit test's results should be consistent; this means that it should always return the same result if nothing is changed between runs.
  • Self-checking: The test should be able to automatically detect if it passed or failed without any human interaction.
  • Timely: A unit test should not take a disproportionately long time to write compared to the code being tested. If you find that testing the code is taking a large amount of time compared to writing the code, consider a design that is more testable.

If your specific scenario does not suit any of these, it's likely that it would be best tested in another way.

Best practices for unit testing

The following guidelines should assist teams in writing effective unit tests that will also appeal to the needs of the testing team.

1. Naming your tests

Tests are useful for more than just making sure that your code works, they also provide documentation. Just by looking at the suite of unit tests, you should be able to infer the behaviour of your code. Additionally, when tests fail, you can see exactly which scenarios did not meet your expectations. The name of your test should consist of three parts:

  • The name of the method being tested
  • The scenario under which it's being tested
  • The behaviour expected when the scenario is invoked

By using these naming conventions, you ensure that its easy to identify what any test or code is supposed to do while also speeding up your ability to debug your code.

2. Arranging your tests

Readability is one of the most important aspects of writing a test. While it may be possible to combine some steps and reduce the size of your test, the primary goal is to make the test as readable as possible. A common pattern when unit testing is "Arrange, Act, Assert". As the name implies, it consists of three main actions:

  • Arrange your objects, by creating and setting them up in a way that readies your code for the intended test
  • Act on an object
  • Assert that something is as expected

By clearly separating each of these actions within the test, you highlight:

  • The dependencies required to call your code,
  • How your code is being called, and
  • What you are trying to assert.

3. Write minimally passing tests

Tests that include more information than is required to pass the test have a higher chance of introducing errors, and can make the intent of the test less clear. For example, setting extra properties on models or using non-zero values when they are not required, only detracts from what you are trying to prove.

When writing unit tests, you want to focus on the behaviour. To do this, the input that you use should be as simple as possible.

4. Avoid logic in tests

When you introduce logic into your test suite, the chance of introducing a bug through human error or false results increases dramatically. The last place that you want to find a bug is within your test suite because you should have a high level of confidence that your tests work. Otherwise, you will not trust them and they do not provide any value.

When writing your unit tests, avoid manual string concatenation and logical conditions such as if, while, for, or switch, because this will help you avoid unnecessary logic.

5. Prefer helper methods to Setup and Teardown

In unit testing frameworks, a Setup function is called before each and every unit test within your test suite. Each test will generally have different requirements in order to get the test up and running. Unfortunately, Setup forces you to use the exact same requirements for each test. While some may see this as a useful tool, it generally ends up leading to bloated and hard to read tests. If you require a similar object or state for your tests, rather use an existing helper method than leverage Setup and Teardown attributes.

This will help by introducing:

  • Less confusion when reading the tests, since all of the code is visible from within each test.
  • Less chance of setting up too much or too little for the given test.
  • Less chance of sharing state between tests which would otherwise create unwanted dependencies between them.

6. Avoid multiple asserts

When introducing multiple asserts into a test case, it is not guaranteed that all of them will be executed. This is because the test will likely fail at the end of an earlier assertion, leaving the rest of the tests unexecuted. Once an assertion fails in a unit test, the proceeding tests are automatically considered to be failing, even if they are not. The result of this is then that the location of the failure is unclear, which also wastes debugging time.

When writing your tests, try to only include one assert per test. This helps to ensure that it is easy to pinpoint exactly what failed and why.

Unit test example - ScalaTest example

However, if you're like me, just knowing the theory doesn't help - and you will soon forget everything that you've just read! So, I want to leave you with a practical example that might help you understand unit tests a little more, while also giving you something to practise. This will hopefully provide you with a better foundation on how to implement unit tests correctly in your existing project work.

The code below is in an application that assigns toppings to pizzas, see how many unit tests you can add to test this code effectively:

Package 1

package com.acme.pizza
import scala.collection.mutable.ArrayBuffer
class Pizza {
  private val toppings = new ArrayBuffer[Topping]
  def addTopping (t: Topping) { toppings += t}
  def removeTopping (t: Topping) { toppings -= t}
  def getToppings = toppings.toList

  def boom { throw new Exception("Boom!") }
}

Package 2

package com.acme.pizza
case class Topping(name: String)

Answer

Below is a generic example of a series of unit tests that test a Scala API which allows you to test the assemblage of a pizza.

Unit test sample:

package com.acme.pizza
import org.scalatest.FunSuite
import org.scalatest.BeforeAndAfter
class PizzaTests extends FunSuite with BeforeAndAfter { 
  var pizza: Pizza = _
  before {
    pizza = new Pizza
  }
  test("new pizza has zero toppings") {
    assert(pizza.getToppings.size == 0)
  }
  test("adding one topping") {
    pizza.addTopping(Topping("green olives"))
    assert(pizza.getToppings.size === 1)
  `}`
  // mark that you want a test here in the future
  test ("test pizza pricing") (pending)
}

Given the foundation of these two basic tests, I would encourage you see how many more tests you can think of!

I hope that the guidelines above have given you:

  • Enough reason to bring testers into the unit testing space, and
  • A better idea of how to write unit tests in a way that will enhance readability and quality.

A fundamental difference remains in the thought patterns and approaches to software development and testing. But there is no reason that both of these disciplines should not work closer together to create a better quality product for customers.



Craig is an avid software architect at Allan Gray. He believes that, to really make a difference in the software world, you should not just focus on what you develop, but also on how you develop it by focusing on quality design and automation. However, he understands that code is more than just a machine language, and that there are real humans behind the process. This is why he chooses to focus just as much time on team development and mentoring, as he does on solving complex technical problems.


When not changing the world of software development, Craig can also be found writing for Critical Hit as well as his own LinkedIn blog, designing board games for his company Risci Games and running long distances for no apparent reason whatsoever. He likes to think that his many passions only add to his magnetic charm and remarkable talent - at least, that's what he tells himself when he goes to sleep at night!


Source-banner--1-

Cat eyes@2x

Subscribe to our blog

Don’t miss out on cool content. Every week we add new content to our blog, subscribe now.