It is, of course, vital to test software. There are various ways to test software, including:
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.
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.
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.
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.
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:
This approach has several advantages:
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.
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.
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.
Copyright (c) Axlesoft Ltd 2020