Unittest
Unittest is a unit testing framework for python that comes as part of python’s standard library. You can start using the unittest
framework by simplying import
ing it using import unittest
.
Structure
The test file naming convention is to use test_
and the name of the file that is going to tested in the format: test_NAME_OF_FILE_TO_BE_TESTED
. This is a requirement for writing tests.
You must also import the code you want to be tested at the head of your test file.
import unittest
import my_code_to_be_tested
To declare a new set of tests for a particular file create a new class that inherits from unitttest.TestCase
class. Your test code will be defined within this class.
class TestMyClass(unittest.TestCase):
To define a new test create a new method the name of which should start with test_
. This is again a requirement for writing tests, with this naming convention telling the test runner which methods are tests.
def test_my_method(self):
The basic structure of a single test follows a pattern of using an assertion method on the self
object with two arguments: the expected output and the actual output.
def test_my_method(self):
self.assertEqual(assertion, expectation)
You can add multiple assertions into a single test. These will appear as a single test pass on your test summaries, however if one of the assertions within a single test breaks the test then the resulting error will direct you to the specific assert that failed the test.
def test_my_method(self):
self.assertEqual(assertion, expectation)
self.assertEqual(another_assertion, another_expectation)
You can create the equivalent of before_each
method in your unit test suite that runs before each test by using the setUp
method. This is run before each test method.
def setUp(self):
# do set up here
You can keep your test code organised into blocks of similar functionality by creating a single test class for a specific set of tests with a single setUp
method and then inheriting a number of different test classes from that class to organise your test code. In the example below we define a TestClass
that inherits from the unittest.TestCase
class and then when we want to organise a specific and more localised test we subclass it with the SomeSpecificTest
class.
class TestClass(TestCase):
def setUp(self):
# general class set up
class SomeSpecificTest(TestClas):
def test_something_specific(self):
# do a specific test
Running tests
You can run all test files in you project (i.e. file beginning with _test
) by using the python3 -m unittest
command and not passing in any file arguments.
$ python3 -m unittest
You can run a single test by calling unittest
from Python as the main module and passing in the test file you want to run.
$ python3 -m unittest test_my_class.py
You can run tests in a specific directory by using the discover
command the passing in the directory name.
$ python3 -m unittest discover my_test_folder
You cannot call testing files directly, such as python3 test_my_class.py
, however, you can set up a test file to run through the unittest
framework automatically when called use the __name__ == '__main__'
pattern by appending the code below to the body of your test file. This will conveniently only run the test that was called with the python
run command and won’t trigger the unittest.main
method for ALL tests.
if __name__ == '__main__':
unittest.name()
Assertions
You can add failure messages to an assertion by adding a third string argument to the assert
method that contains the message to be printed on failure.
def test_equal(self):
self.assertEqual(6, 6, "it should equal 6")
You can test if an object is an instance of class by using the assertIsInstance
method.
def test_is_instance(self):
self.asssertIsInstance("Hello", str)
You can test if a class has a method or attribute by using the hasattr
method in conjunction with an assertTrue
method.
def test_has_attribute(self):
self.assertTrue(hasattr(self.object, "some_attribute_name"))
Mocks
You can start using mock and double objects by using the python unittest.mock
extension library. These mocks give you the ability to stub methods on real classes as well as create entire mock / double objects for the purposes of testing and keeping your dependencies separate and testable. They also provide assertion functions for testing that a specific method was called. The example import
statements below show several different ways you could import
mocks into your tests depending on your preferences.
# import top level mock namespace
import unittest.mock
# import mock namespace
from unittest.mock import mock
# import specific frequently used mocking modules
from unittest.mock import Mock
from unittest.mock import MagicMock
Mock Objects
You can create a mock object by instantiating an instance of the Mock
or class.
def test_my_mock(self):
my_mock = Mock()
You can test that a method was called on a mock by appending the name of the method you want to test for to the mock instance and then using the assert_called_with
method on mock method property. Mock objects automatically create a mock version of a method if it is called which means you don’t have to explicitly specify that my_mock
accepts my_method
and the assertion on its call will still work.
# passes
def test_mock_method_called(self):
my_mock = Mock()
my_mock.my_method()
my_mock.my_method.assert_called_with()
You can test that a mock method was called with a specific set of arguments by submitting the expected arguments to the assert_called_with
method.
# passes
def test_mock_method_called_with_args(self):
my_mock = Mock()
my_mock.my_method(2, 3)
my_mock.my_method.assert_called_with(2, 3)
You can set a return value for a mock’s method by setting the return_value
property of a mock’s method property. When that mock property is called as a method it will return whatever was set as its return.
# passes
def test_mock_method_called_with_return(self):
my_mock = Mock()
my_mock.my_method.return_value = 35
self.assertEqual(my_mock.my_method(), 35)
Mock Stubs
You can stub a real object’s methods by assigning the object’s method property to an instance of Mock
.
# passes
def test_stub_method(self):
real_instance = RealClass()
real_instance.real_method = Mock()
real_instance.real_method()
real_instance.real_method.assert_called_with()
You can stub the return value of a real object’s method by assigning the return_value
method property of the object that you are mocking.
# passes
def test_stub_method(self):
real_instance = RealClass()
real_instance.real_method = Mock()
real_instance.real_method.return_value = 35
self.assertEqual(real_instance.real_method(), 35)
Magic Mock
The mock
module’s MagicMock
class is similar to the Mock
class apart from that it supports mocking for Magic Methods (also called Dunder methods). These special functions can usually NOT be reassigned or stubbed in Python but instances of MagicMock
has special functionality that allows them to stub these Magic Methods with predefined return types. This useful if you want to mock things like arithmetic or comparative operators on an object and then make assert_called_with
tests against those operators.
To start using MagicMock
simply import
it from the unittest.mock
module.
from unittest import mock
magic_mock = mock.MagicMock()
You can mock a values comparative operations by reassigning the associated magic methods to a mock
with a hard coded return_value
. In the code below we defined a mock_number
as an instance of MagicMock
we then create a mock __eq__
method that is an instance of Mock
and is hard coded to return true. We can then make test assertions against this mock __eq__
method to check if it was called in our code with assert_called_with(1)
. Furthermore the if
statement shows that the mocked __eq__
method still works like a regular ==
comparator.
def test_comparator_operator_mock():
mock_number = mock.MagicMock()
__mock_eq__ = mock.Mock()
__mock_eq__.return_value = True
mock_number.__eq__ = __mock_eq__
if(mock_number == 1):
print("Mock __eq__ function returned 1.")
mock_number.__eq__.assert_called_with(1) # => True
The same logic applies to mocking a values arithmetic operators by reassignment to mock
objects.
def test_arithmetic_operator_mock():
mock_number = mock.MagicMock()
__mock_add__ = mock.Mock()
__mock_add__.return_value = 2
mock_number.__add__ = __mock_add__
result = mock_numer + 1
print(result) # => 1
mock_number.__add__.assert_called_with(1) # => True
Coverage
You can track python test coverage using the coverage.py
module. You can install coverage.py
using pip install
or pipenv install
.
$ pip install coverage
$ pipenv install coverage
Tracking coverage
The basic flow of coverage.py
is to
- Run your python tests using
coverage
and collect data on test coverage - Report the test coverage data
Unlike other test coverage frameworks these are divided into separate command line steps to execute. (The following examples show usage using pipenv
, however they are quite similar to the standard CLI usage in the coverage.py
docs. To run tests with coverage data gathering use the run
command with coverage
. This will create a .coverage
file in your repo that you should probably add to your .gitignore
.
$ pipenv run coverage run -m unittest
To report the coverage data using the report
function which will print a nice table showing test coverage
$ pipenv run coverage report -m
If you want to run both the coverage data gathering and reporting together you can combine both commands into a bash .sh
script file.
#!/bin/bash
pipenv run coverage run -m unittest
pipenv run coverage report -m
Configuration
You can configure your coverage settings by creating a .coveragerc
file.
Excluding
You can omit a directory from being include in your coverage report by adding it to the omit
list under the [run]
section of your .coveragerc
file. An omission needs to be placed on a new line after the omit
flag and indented by at least one tab. The *
asterisk indicates a match all. In the example below we omit
anything within the .local
directory.
# .coveragerc
[run]
omit =
*/.local/*
You can omit a file by a naming pattern from being include in your coverage report by adding to the omit
list with the file name matched using the *
asterisk character. The omit
below ignores any __init__.py
files.
# .coveragerc
[run]
omit =
*__init__*
You can have multiple omit lines by simply carriage returning the different omit
entries.
# .coveragerc
[run]
omit =
*/.local/*
*__init__*
Coveralls
Coveralls is a service for creating coverage badges for your repos. There is a wrapper for coveralls for python that interfaces directly with Travis CI and the coverage.py
module. To install coveralls simply run a pip
command on the python-coveralls
package.
$ pipenv install python-coveralls
Coveralls currently ONLY WORKS with coverage.py
versions less than 5.0
. You must set up your pipfile
to only install coverage="<5.0"
with semantic versioning if you want coveralls to work.
After installing python-coveralls
and adding it to your pipfile
. The only thing you need to do is add the coveralls
command your .travis.yml
file in the after_success
script section so that it sends coverage data once completed.
# .travis.yml
language: python
python:
- "3.7"
-
install:
- pip install pipenv
- pipenv install
script:
- pipenv run coverage run -m unittest
after_success:
- coveralls