What is unit testing?


Martin McBride, 2020-08-04
Categories unit testing testing

It is, of course, vital to test software. There are various ways to test software, including:

  • Testing that the individual software components of the system work correctly. This can include anything from testing an entire sub-system, or testing individual modules.
  • Testing the whole system - this can take the form of integration testing, end-to-end testing, and functional testing.
  • Specialised testing - for example, acceptance tests are often used as part of a development contract to prove that the software is fit for purpose, performance testing proves that the software has the capacity (including processing power, memory and disk space and so on) to do its job, and penetration testing to check the the system is secure against hackers.

Software tests can also be characterised as automated (tests that can be set running and left to complete without intervention) or manual (test that require manual involvement, for example a human operator using the system to perform various tasks and checking the results).

Unit testing is automated testing that checks individual software modules at the lowest level - often testing individual classes and functions in isolation.

Benefits of unit testing

The basic idea behind unit testing is that it is far easier to fix bugs in an individual class than it is to fix that same bug if it appears in a system that has dozens of classes interacting.

If a particular class isn't working properly, then there is a very good chance that it will cause a bug in the overall system. Therefore if you can identify the bug by testing the class isolation, you have almost certainly saved yourself a lot of extra work when you start integration testing.

And in some cases, you might have identified a problem that would only show up rarely or intermittently. Problems like that can be very expensive to fix, so the more of them you can eliminate at the unit testing level, the better.

There are several other benefits which we will explore later in this article.

What is a unit test?

A unit test is generally a stand-alone piece of code that exercises a single class, supplying various inputs, checking the results, and reporting success or failure. A unit test will usually test all the methods of the class, supplying normal values, edge values, and invalid values, and making sure the method either provides the correct result, or fails in the expected way (for example by throwing an exception).

Although it is possible to write unit tests from scratch, there are several unit testing modules available in Python to make life easier. They allow you to define suites of unit tests, and control which ones are executed using command line flags. They also have built in convenience functions, such as assert statements for comparing actual and expected values.

Often a particular class will call other classes as part of its behaviour. Since we are aiming to test each class in isolation, we try to avoid unit tests that test the behaviour of several classes at the same time. Instead we use mocking. A mock object is an object that behaves like a real object in the project, but actually it is just a simple class that records how it is called, and returns predefined results. The mock object does exactly what is necessary to check that the object under test is working correctly. This allows us to test a class without relying on other classes.

Again, you can write your own mock classes, and sometimes that makes sense. But there are Python modules that provide quite sophisticated mocking functionality without you having to write it yourself.

Automated unit tests

Wherever possible, it is good to make the tests automated. Ideally they should also run fairly quickly too.

This allows you to run the suite of unit tests automatically at certain times. That might be each time code is checked in our built, or it might be every night.

The advantage of this is that any code changes that have altered the expected behaviour will be flagged up very quickly. It is much easier to find a problem if it is reported within a few hours, it is often simply a matter of looking at the changes that have been made in that time.

To reiterate, unit tests do not replace other forms of testing, but they are a good way of eliminating a lot of module level bugs very quickly.

Test driven development

Testing is often left to the end of a project, but unit testing is different - it is actually possible (and beneficial) to write the unit tests before you write the code.

Typically the process works like this:

  • Design the class or module - what methods or functions it has, and what they do.
  • Create a class with stub methods that do nothing and return dummy values.
  • Design and write the unit tests. At this point you will have a full set of test that (hopefully) will all fail.
  • Start writing the code for your class. As you proceed, more and more of the unit test will start to pass until the class is complete.

This approach has several advantages:

  • Looking at the class from a second angle - how to test it - can help highlight deficiencies in the interface design.
  • Identifying all the edge cases at the start can improve the code quality (and having tests in place makes sure none of the edge cases are forgotten).
  • As the class is developed, the number of unit test errors reduces, giving an estimate of progress.
  • Last but not least, if you create the tests first, they are done and you will continue to benefit from them throughout the life of the module. If you leave the unit tests to the end, there is always a temptation to not do them and move on to the next class.

Unit testing and refactoring

Refactoring means changing code to make it more readable, or more maintainable, without changing its functionality.

Refactoring code is generally a desirable thing to do, because it is a process of continuing improvement. But there is always a risk that making changes to the structure of a module might actually change the functionality even when it wasn't supposed to.

With more traditional end-of-project functional testing, refactoring may well result in a new bug that isn't spotted until long after the change was made, and it can take many hours to track the bug back to the change that caused it. Or even worse, the real cause might never be found, and the bug might be fixed by hacking some other part of the code as a work-around.

With unit testing in place, it is possible to refactor code with much more confidence that the new code still has exactly the same functionality as before. This, of course assumes that your unit tests are sufficiently comprehensive to cover all possible paths through the code.

In practice, refactoring code sometimes involves changes to behaviour as well as simple code improvements. For example, a particular class might be judged to be doing to many different things, and requires splitting into two separate classes. In that case, it is often a good idea to create unit tests to match the new functionality before you start changing the code.

Test driven design

As you apply unit tests, you will probably start thinking about unit tests as you design your system. With experience you will take testability into account as one factor when deciding how to decompose your program into classes or modules.

This is called test driven design. It is generally a good thing - the sort of qualities that make a class easy to test are often the same sort of qualities that make it a good class in ever other way. Test driven design will tend to happen naturally and can turn out to be a virtuous circle.

Summary

It takes a certain amount of commitment to embark on unit testing, especially a test first approach. But it should pay dividends in terms of code quality and total development time right from the start, and throughout the life of the project.


Tag cloud

2d arrays abstract data type alignment and array arrays bezier curve built-in function close closure colour comparison operator comprehension context conversion data types design pattern device space dictionary duck typing efficiency encryption enumerate filter font font style for loop function function composition function plot functools generator gif gradient higher order function html image processing imagesurface immutable object index inner function input installing iter iterator itertools lambda function len linspace list list comprehension logical operator lru_cache mandelbrot map monad mutability named parameter numeric python numpy object open operator optional parameter or partial application path positional parameter print pure function radial gradient range recursion reduce rotation scaling sequence slice slicing sound spirograph str stream string subpath symmetric encryption template text text metrics transform translation transparency tuple unpacking user space vectorisation webserver website while loop zip

Copyright (c) Axlesoft Ltd 2020