Each language has its own known testing framework which makes writing test cases easier. In
Python, the standard and built-in are unittest
module. Together with pytest
or coverage
,
we can get a statistics of all the tests which can be displayed in GUI.
test cases in unittest
- Implement a class which inherits from
unittest.TestCase
- Add tests
- Call related
assert*
methods to check results
- Call related
- Add an entrance for the testing module
import operator
import unittest
class SimpleTestCase(unittest.TestCase):
def test_add(self):
a = 2
b = 3
self.assertEqual(5, operator.add(2, 3))
if __name__ == "__main__":
unittest.main()
Running the test
We have added __name__
related code, so we can directly call the module. Assuming previous
code is saved in a file named simple_test.py
, we can run:
# Run all the tests in the module
python -m unittest simple_test[.py]
# Run a single test
python -m unittest simple_test.SimpleTestCase.test_add
If simple_test.py
is saved in a different folder which means in a different module, we need
to make sure that the testing module is importable. If simple_test
is saved under a folder
named tests
, then there should be a __init__.py
under that.
python -m unittest tests/simple_test.py
Before and after a test
If there are codes we need to run before a test (initialization code) or after a test (cleanup
code), we can override setUp
and tearDown
.
import operator
import unittest
class SQLDB(object):
def connect_db(self):
pass
def read_record(self):
return 1
def disconnect_db(self):
pass
class SimpleTestCase(unittest.TestCase):
def setUp(self):
self._db = SQLDB()
self._db.connect_db()
def tearDown(self):
self._db.disconnect_db()
def test_add(self):
self.assertEqual(1, self._db.read_record())
if __name__ == "__main__":
unittest.main()
Skip certain tests
If there are tests we want to skip under certain condition, we can skip it instead of emitting
an error by running it. We can skip a whole test and any single test by decorating with
unittest.skipIf
import operator
import os
import unittest
try:
import SQLDB
except ModuleNotFoundError:
SQLDB = None
class SQLDB(object):
def connect_db(self):
pass
def read_record(self):
return 1
def disconnect_db(self):
pass
@unittest.skipIf(SQLDB is None, "SQLDB is not available")
class SimpleTestCase(unittest.TestCase):
def setUp(self):
self._db = SQLDB()
self._db.connect_db()
def tearDown(self):
self._db.disconnect_db()
@unittest.skipIf("DB_TEST" not in os.environ, "Not a testing environment")
def test_add(self):
self.assertEqual(1, self._db.read_record())
if __name__ == "__main__":
unittest.main()
There are other skipping decorators:
skip(reason)
- Used to write customized skipping decorators
# Refer to https://www.chunyangwen.com/blog/python/python-descriptor-decorator.html
# for more details about a decorator
def skipUnlessHasattr(obj, attr):
if hasattr(obj, attr):
return lambda func: func
return unittest.skip("{!r} doesn't have {!r}".format(obj, attr))
skipUnless(condition, reason)
skipIf(condition, reason)
unittest.expectedFailure
: A failure is a success.
Asserting methods
Method | Checks that |
assertEqual(a, b) | a == b |
assertNotEqual(a, b) | a != b |
assertTrue(x) | bool(x) is True |
assertFalse(x) | bool(x) is False |
assertIs(a, b) | a is b |
assertIsNot(a, b) | a is not b |
assertIsNone(a) | a is None |
assertIsNotNone(a) | a is not None |
assertIn(a, b) | a in b |
assertNotIn(a, b) | a not in b |
assertIsInstance(a, b) | isinstance(a, b) |
assertNotIsInstance(a, b) | not instance(a, b) |
Raises or warnings | |
assertRaises(exc, fun, *args, **kwargs) | fun(*args, **kwargs) raise exc |
assertRaisesRegex(exc, r, fun, *args, **kwargs) | fun(*args, **kwargs) raise exc, message matches regex r |
assertWarns(warn, fun, *args, **kwargs) | fun(*args, **kwargs) raise exc |
assertWarnsRegex(warn, r, fun, *args, **kwargs) | fun(*args, **kwargs) raise warn, message matches regex r |
assertLogs(logger, level) | The with block logs on logger with minimum level |
Number related | |
assertAlmostEqual(a, b) | round(a-b, 7) == 0 |
assertNotAlmostEqual(a, b) | round(a-b, 7) != 0 |
assertGreater(a, b) | a > b |
assertGreaterEqual(a, b) | a >= b |
assertLess(a, b) | a < b |
assertLessEqual(a, b) | a <= b |
assertRegex(s, r) | r.search(s) |
assertNotRegex(s, r) | not r.search(s) |
assertCountEqual(a, b) | a and b have the same elements in the same number, regardless of the order |
Sequence related | |
assertMultLineEqual(a, b) | strings |
assertSequenceEqual(a, b) | sequences |
assertListEqual(a, b) | lists |
assertTupleEqual(a, b) | tuples |
assertSetEqual(a, b) | sets or frozensets |
assertDictEqual(a, b) | dicts |
Common practices
# assert whether certain log happens
with self.assertLogs(logger, level="INFO") as cm:
# bla bla
self.assertRegex(" ".join(cm.output), ".*xxx.*")
with self.assertRaises(ValueError):
# bla bla
unittest.mock
Mock is very important is tests.
- You don’t want your test cases fail due to an unrelated service.
- You don’t want your test cases coupled with each other:
- Testing functionalities of
a
whileb
may fail.
- Testing functionalities of
A simple case
Usually we will use unittest.mock.patch
to help us write tests. We can patch:
- constants
- Actually I think this is easy done without
mock
.
- Actually I think this is easy done without
- a module function
- a class method
- a static method
- an object method
# my_module.py
import os
def get_more_magic_values():
return [42] * 2
def get_magic_value():
return get_more_magic_values()[0]
def listdir(p):
return os.listdir(p)
class ObjectToBeMocked(object):
HelloWorld = 3
def hello(self):
return ObjectToBeMocked.HelloWorld
def hi(self):
return self.hello() + 1
@property
def beer(self):
return -1
@classmethod
def oops(cls):
return "oops"
We save above code into a file named my_module.py
. Most of the functions and methods
are just naively implemented. So how we can test it?
import unittest
from unittest import mock
from unittest.mock import patch
from my_module import ObjectToBeMocked
import my_module
class MyModuleTest(unittest.TestCase):
@patch("my_module.ObjectToBeMocked.HelloWorld", 42)
def test_hello(self):
self.assertEqual(42, my_module.ObjectToBeMocked().hello())
@patch.object(my_module.ObjectToBeMocked, "hello", return_value=42)
def test_hi(self, hello):
o = ObjectToBeMocked()
self.assertEqual(43, o.hi())
@patch.object(my_module.ObjectToBeMocked, "hello")
def test_hi_1(self, hello):
hello.return_value = -1
o = ObjectToBeMocked()
self.assertEqual(0, o.hi())
@patch("my_module.get_more_magic_values")
def test_get_magic_value(self, get_more_magic_values):
get_more_magic_values.return_value = [41]
self.assertEqual(41, my_module.get_magic_value())
@patch("my_module.ObjectToBeMocked.beer", new_callable=mock.PropertyMock())
def test_property_beer(self, prop):
prop.return_value = -2
self.assertEqual(-2, my_module.ObjectToBeMocked().beer())
@patch("my_module.os")
def test_mock_os(self, mock_os):
mock_os.listdir.return_value = ["hi", "hello"]
self.assertListEqual(["hi", "hello"], my_module.listdir("."))
Notes
- We have to mock the module where it is used. NOT from a general place
- Please refer to
mock_os
- Please refer to
- When it comes to an instance,
patch.object
is used instead ofpatch
. - Difference between
patch
andpatch.object
:patch.object
mocks a single method or attributes, target is also the class
classes
unittest.mock.Mock
unittest.mock.MagicMock
unittest.mock.PropertyMock
unittest.mock.AsyncMock
Common practices
# create a side-effect
with patch(Clz, "method", side_effect=ValueError) as mock_method:
ins = Clz()
self.assertRaise(ValueError):
ins.method()
from unittest.mock import create_autospec
# automatically behaves like `Clz`
MockObject = create_autospec(Clz)