Python is a multi-purpose language that is used for everything backend. In this article, I will teach you to perform basic unit testing in Python, how to mock modules, and make sure your code is clean.
What is Unit-testing?
Unit-testing is one of the ways to test your code. Other ways include functional testing, integration testing, regression testing and so on. Testing is vital to any larger codebase, as it lets you iterate and perform changes quickly, without worrying to much about what is going to break.
Unit-testing is the lowest testing method on the abstraction level. Unit-testing is concerned with testing individual modules and functions in isolation. That is, by making sure all the parts of your system work correctly, you can make the assumption that the whole system is working ok.
That is, in ideal world, of course. While unit-testing is very valuable, the whole system is not merely a sum of its parts: even if every function is working as intended, you will still need to test how well do they fit together (which is out of scope of this article).
How does Unit-testing relate to TDD?
Unit-testing is one of the foundations of TDD. TDD stands for Test Driven Development, and is a methodology for producing quality software. In essence, it all boils down to these:
- Write tests first. Think about how different parts of your system work in isolation and write tests that validate their intended behaviour. Your tests must fail, because you have not written any actual code yet.
- Write just enough code to make the tests pass.
- Refactor what you just wrote.
- Go to step 1.
That is it! The whole cycle takes a couple of minutes, but this simple technique will make sure your system is (1) working according to the specs and (2) is testable. But to get started with TDD, you need to master basic unit-testing first.
unittest
module
Unit-testing in Python is available out of the box with the unittest
module. We will learn unit-testing by developing our own factorial function. To get started, open a new Python file add write this code it it:
On line 1 you can notice the imported unittest
module. On lines 4-6 we define a test case by extending the TestCase
class. In it, we only have 1 test at the moment, the test_something
. It is important that tests are prefixed with test_
, so that the test runner can find them. Finally, on lines 9-10 we execute the tests. If you try running this file now, the test will fail:
That is happening because on line 6 we are asserting that True
and False
are equal, which is obviously false. Let’s change that to test our (unwritten!) function:
If you run this now, it will still fail, because we did not write the factorial
function yet. Let’s make this test pass:
def factorial(num):
return 6
While this is not mathematically sound, this makes our test pass. Let’s write some more tests:
def test_factorial(self):
self.assertEqual(factorial(1), 1)
self.assertEqual(factorial(2), 2)
self.assertEqual(factorial(3), 6)
self.assertEqual(factorial(10), 3628800)
Check that the tests now fail (since factorial
returns 6 all the time) and make the changes to the factorial function:
def factorial(num):
if num == 1:
return 1
return num * factorial(num - 1)
And, if you run this, the tests pass:
Ran 1 test in 0.002s
OK
Now, let’s take a step back and understand what we just did.
Assertions
When we write tests, we want to test for something. If something is equal, or not equal, if a function throws an exception, if a method was called with certain parameters, etc. Such checks are called assertions. Assertions are things that are always supposed to be true for the code to work properly.
One assertion you just saw if the assertEqual
. It checks that 2 variables passed in are equal to each other. assertEqual
is available through self
, and is provided by the TestCase
parent class. Here are some of the common assertions you will find useful:
assertEqual(x, y)/assertNotEqual(x, y)
assertTrue(x)/assertFalse(x)
assertIs(x, y)
x is y
assertIsNone(x)
assertIn(x, y)
- Checks if
x
isin
y
- Checks if
assertIsInstance(x, y)/assertNotIsInstance(x, y)
- Checks if
x
an instance ofy
- Checks if
assertRaises(exc, fun, *args, **kwargs)
- Checks if
fun
raises an exceptionexc
when called with*args, **kwargs
- Checks if
assertGreater(x, y)/assertLess(x, y)/assertGreaterEqual(x, y)/assertLessEqual(x, y)
Some of these are available using a context manager, like assertRaises
. It makes the code a bit easier to read:
with self.assertRaises(Exception):
do_something_that_throws("please")
Using these assertions you can test pretty much anything!
Mocking
Recall that unit testing is testing individual parts of system in isolation. But, this is never the case with the systems we design. Different parts depend on other parts, and to make testing possible you sometimes need to mock them out. For example, if you are testing the behaviour of an HTTP response parser, there is no need to perform an actual HTTP response: all you need to do it mock it.
Mocking, then, is a method of abstracting implementation of software modules that are not relevant to the behaviour you are testing. Moreover, you can assert if a mock was called, how many times was it called, and what arguments were supplied to it. These additional assertions will, no doubt, make your tests more robust.
Mocking in Python is also available via the unittest
module. Let’s start with a simple example. Suppose you wrote a function that calls the supplied callback:
def call_this_function(func):
func()
To test it, you just need to pass a MagicMock
(available through unittest.mock
):
MagicMock
is a special type of object. You can call it itself, call any method you can come up with and it will never throw. Instead, it will memorize all calls and make them available to you through assertions. Note that in this case, assertions are called on the mock
object, instead of self
. Here are some of the assertions available for mock objects:
assert_called
assert_called_once
assert_called_with
assert_called_once_with
assert_not_called
Mocking imports
Sometimes, when you are testing a class, you need to mock out a certain function or class that is imported in it. Consider this example:
Now, to test this code, you want to mock the api_action
function. This is actually very easy to do with the patch
decorator (available from unittest.mock
):
The patch
decorator is applied to the test function that we are working with. It accepts as an argument the path to the mocked module. There is a very important note here: you specify the path relative to the testing code. For example, the main.py
file imported the api_action
function from some_library
. Then, we import the main
module into our test file. This means that we mock the api_action
function that is imported inside the main
, hence the 'main.api_action'
path. If we were to write 'some_library.api_action'
, this would not work. We also specify the side_effect
argument, which is essentially the return value that will be returned by the mock. The mock object is then passed as an argument to test function so we can assert on it. We assert that (1) the return value is forwarded and (2) that api_action
is called on the correct endpoint.
Closing notes
Thank you for reading this article, I hope you liked it. Let me know in the comments about your experience with testing in Python!