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.
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
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.
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)
x is y
- Checks if
- Checks if
assertIsInstance(x, y)/assertNotIsInstance(x, y)
- Checks if
xan instance of
- Checks if
assertRaises(exc, fun, *args, **kwargs)
- Checks if
funraises an exception
excwhen called with
- 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!
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
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:
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
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.
Thank you for reading this article, I hope you liked it. Let me know in the comments about your experience with testing in Python!