Python unittest Cheatsheet
Python is a great language with lots of awesome libraries and resources. Most of those resources are beginner-focused though, and spend lots of words to say very little. Not to mention the awful advertisements, pop-ups, paywalls, etc.
The official docs on
unittest
are good, but it feels too encyclopedic, and it’s hard to find a
quick overview.
In this post I’ll give you just that, a quick and to the point overview on how
to test with Python’s built-in unittest
framework.
Test Discovery
Python’s unittest
module includes test discovery, which means it will scan
your project folder and find all relevant test files to run.
You can customize it if you want, the convention is this:
- Put your tests into a
test
directory in your project root - The
test
directory must be a valid package, and include a__init__.py
- All sub-packages must also include a
__init__.py
- Each module you want to test must start with
test
, for example:test_my_class.py
- Each test method must start with
test
Here’s an example directory structure:
test
├── __init__.py
└── my_package
├── __init__.py
├── game
│ ├── __init__.py
│ └── core
│ ├── __init__.py
│ ├── test_pubsub.py
│ ├── test_sprite.py
│ └── test_vector.py
├── parser
│ ├── __init__.py
│ ├── test_input.py
│ ├── test_mapfile_parser.py
│ └── test_parser.py
└── test_models.py
And here’s an example test file:
import unittest
from engine.game.core.vector import Vector
class TestVector(unittest.TestCase):
def test_add(self):
v1 = Vector(2, 3)
v2 = Vector(1, 1)
v3 = v1 + v2
self.assertEqual(v3.x, 3)
self.assertEqual(v3.y, 4)
With that in place, you can run your tests by simply running python -m
unittest
.
You can see all of the assert methods in the official documentation.
Mocking
The unittest
module includes great mocking support. When mocking, I like to
follow a few rules:
- Never mock the object under test
- Mock as little as possible
- Make sure the mock object mimics the implementation of the real object
We can easily do all this with unittest.mock
:
import unittest
import appgamekit as agk
from unittest.mock import patch, Mock
from engine.game.core.sprite import Sprite
from engine.game.core.image import Image
class TestSprite(unittest.TestCase):
@patch('engine.game.core.sprite.agk', spec=agk)
def test_dispose(self, agk):
agk.create_sprite.return_value = 7
image = Mock(spec=Image, id=1)
sprite = Sprite(image)
sprite.dispose()
agk.delete_sprite.assert_called_once_with(7)
image.dispose.assert_called_once()
You can read the official documentation for more details, but the gist is this:
- You can use the
@patch
decorator to replace anything with a mock (so you don’t need dependency injection, although it should be preferred as it’s better object oriented design) - You can use
Mock
to create a mock object, which will mimic the object passed inspec=
. This is very important, as if we ever change the real object, we want our tests to complain
In our code, agk
is an external
library which creates a
game window, sprites, sounds, text, and all that’s required to create a game.
We don’t want to test the real thing in a unit test (we would do it in an
integration test), so we mock it away.
We pass the spec
parameter to the @patch
decorator so we make sure the
test will raise an error if we use something in the mock that doesn’t exist in
the real object.
We set the return value of the agk.create_sprite
method, and also create a
mock for Image
, which is a dependency of Sprite
. Note that we also pass
spec
there, and an id
attribute which will be equal to 1
.
We can then assert our mocks were called in the way we expect to finish our little test.
That’s it!
If you want to know more about testing in general, feel free to check out my other blog post: OOP Fundamentals: Quick and Dirty Guide to Testing.
For the complete documentation on Python’s unittest
, check out the official
documentation.