Before we jump into the how let us first talk about what unit testing is and why you should be using it for all development. Unit testing is the creation of programmatic tests to execute small pieces of code (units).
You might be starting out in Python or a seasoned developer, unit testing is a key skill all good developers need.
Benefits of unit testing
Unit tests have several benefits:
- Small pieces of code can be tested prior to being handed over to QA and into UAT testing. This allows you to catch any issues early.
- Developers can discover and fix bugs rapidly.
- When someone has discovered an issue/bug and reported it then a unit test can be created to recreate the issue and follow it up for resolution.
- Code quality can be improved and measured. You can determine how much of the code is covered by unit tests which can be a helpful metric.
- Unit tests allow developers to understand the code even if they have not seen it before, they are able to follow the tests and see how the code works.
- It causes developers to think in a more SOLID way, creating small units of code which can be tested in isolation.
- Running unit tests does not require you to set up a full-scale test platform. This is possible by using approaches such as mocking.
How do unit tests work?
Unit tests are programmatic tests which execute small pieces of code. Normally in development this would be testing a single function in isolation to make sure it follows the correct processes. The measure of the test is based on the expected outcome based on the specified parameters.
Let us look at a simple example, below is some pseudo code to print a name on the screen and a message based on the time of day (morning, afternoon, and evening). The method takes in time and name and returns a string containing the message to print to the screen.
Function GetMessage(datetime time, string name) String message = “Hello “ + name + “ and “ If(time < midday) message += “good morning.” Else if(time > midday and time < evening) message += “good afternoon.” Else message += “good evening.” Return message
What to cover with unit testing
Now, let us look at what we might want to test, a few ideas:
- Does the output have the name we passed in?
- If I send a time of 09:00 does the message end with good morning?
- If I send a time of 13:00 does the message end with good afternoon?
- If I send a time of 17:00 does the message end with good evening?
- What happens if I do not pass any time?
All of these are scenarios which you could cover with unit testing. Below is an example of the pseudo code which you could use to test the first scenario:
Function Test_GetMessage_UsesName() String name = “Bob” Result = GetMessage(09:00, name) Assert Result.contains(name”)
An assert stands for an assertion which means something which is proven to be true. In the case of unit tests this means a test will fail if the assertion is not true. Running through the code above, we are creating a variable to hold a name (this is known as arrange). We then call the method we want to test (also known as Act), the method we are testing is sometimes called the “unit under test”. Then finally we check that we have the result we expected (also known as Assert).
All unit tests consist of 3 stages:
- Arrange – this is where the test is prepared. For example, you may set up variables to pass in, create mocks for objects which you do not want to really call and want to control the results, fixtures etc.
- Act – This is the smallest part of the test and is calling the method you are testing. You may see some tests describing this as the subject under test (SUT) or unit under test.
- Assert – This is when the results of the test are examined and the test either passes or fails. If you are following SOLID to the letter and clean code, then you should only have one assert per test.
From the scenarios we created above you can imagine that you will normally want to write multiple tests to cover the same unit. In our case we have identified multiple tests we want to write to test the times of day and names passed in and we expect different outcomes.
What is code coverage and is it a useful metric?
Code coverage in its simplest form is a measurement of how much of your code is covered by at least one unit test. It is a useful metric to have as you should aspire to the highest amount of code coverage possible. A higher rate of coverage should result in fewer bugs and higher code quality.
That said, it is not safe to treat code coverage as an exact truth of quality and unit tests. Our pseudo code from earlier is a single test which covers a reasonable amount of the code. We could now do 2 more tests using the same name (Bob) and pass some different times. Now we would be looking at 100% code coverage and from a metric it does not get any better than that. You will notice though, even though we have great code coverage that we have not tried passing a name, passing a different name, sending in an invalid time (such as 25:00). There are more scenarios we have not tested though we still have 100% coverage.
So, code coverage is a useful metric, it should not be used as the only measure of the amount of testing taking place and validity of the tests. Good developers will strive for a high percentage of code coverage as well as testing a range of scenarios.
What is negative testing?
Negative testing is carrying out a test which you expect will fail, based on not following the expected path. Positive testing in contrast is when a user or code navigates through the expected path. A good example of a negative test might be closing an application halfway through, passing in 25:30 into a function which is expecting a valid time provided.
Negative testing is often overlooked both when carrying out manual and automated testing. It is easy for developers to code for the safe, happy path but as everyone knows users or systems may decide to follow their own route.
A good tester (and developer alike) will make sure they consider as many scenarios as possible (or sensibly possible) which they can execute. This allows developers to write their code in a protective way to help deal with the unexpected. It is unlikely that you will be able to determine every scenario, but you can often cover a suitable number.
What other types of tests are there?
Lots of different organisations have different names for types of tests. Here are a few examples which I use:
- Integration tests – This is when you want to link up your code to one or more existing systems (in the test).
- Service tests – These are tests which test larger pieces of code and user journeys.
- User acceptance testing – This is when the end user or a representative tests the software based on their expectations. This can sometimes help discover more negative tests and unexpected scenarios.
- Business acceptance testing – Tests to check that the software meets the goals set out in the original design and requirements.
- End to end testing – Testing of software with full journeys and all systems connected.
The icons in this page come from icons8.com which is a great source of icons for many purposes.