Unit Tests are software programs written to exercise other software programs (called Code Under Test) with specific preconditions and verify the expected behaviours of the CUT.
Unit tests are usually written in the same programming language as their code under test.
Each unit test should be small and test only limited piece of code functionality. Test cases are often grouped into Test Groups or Test Suites. There are many open source unit test frameworks. The popular ones usually follows an xUnit pattern invented by Kent Beck E.g. JUnit for Java, CppUTest for C/C++.
Unit tests should also run very fast. Usually we expect to run hundreds of unit test cases within a few seconds.
Why are unit tests usually written in the same programming language as the code under test?
We use Python's standard unittest
module (was called PyUnit
) as the example here.
%%file testwrap.py
import unittest #& yellow
from textwrap import TextWrapper
class TestForTextWrapping(unittest.TestCase): #& yellow
def test_no_wrapping_when_string_length_is_less_than_line_width(self): #& yellow
wrapper = TextWrapper(width=10)
wrapped = wrapper.wrap("a" * 5)
self.assertEqual(["a" * 5], wrapped) #& yellow
if __name__ == '__main__':
unittest.main()
Writing testwrap.py
This unit test tests the TextWrapper
class from the standard Python module textwrap
. Of course usually it doesn't make sense to test the standard modules. Those modules have their own tests. Here we just use it as an example for how to write unit test in Python.
Creating a Python unit test is easy.
unittest
module, or TestCase
from the unittest
module.unittest.TestCase
class. The derived class is a test group (Yes, the names are a bit confusing).test_
will be considered as a test case in the test group.assertEqual
, assertTrue
, assertFalse
.Usually, in an xUnit framework, for an assertion with 2 parameters, the convention is that the 1st argument is the expected result and the second argument is the actual result. But somehow in python unittest
module this is not emphasized.
To run unit test cases you need to call unittest.main()
. Then the unittest
framework will run through all the unit tests it can find. In the above example, we already put it in the same .py file. So we can simply run it with the
python -munittest <test_module_name>
command, or
python <test_module_name>.py
(since we added unittest.main()
in that module)
!python testwrap.py
. ---------------------------------------------------------------------- Ran 1 test in 0.001s OK
If the test passes, it should just print OK (and perhaps some dots to show the progress). No other information.
Rule of thumb:
No human intervention should be needed, either to get ready for the test, running the test cases, or checking the result.
Adding a failing test case.
%%file testwrap.py
import unittest
from textwrap import TextWrapper
class TestForTextWrapping(unittest.TestCase):
def test_no_wrapping_when_string_length_is_less_than_line_width(self):
wrapper = TextWrapper(width=10)
wrapped = wrapper.wrap("a" * 5)
self.assertEqual(["a" * 5], wrapped)
def test_no_wrapping_when_string_length_is_more_than_line_width(self): #& white
wrapper = TextWrapper(width=10) #& white
wrapped = wrapper.wrap("aaaaa bbbbb") #& white
self.assertEqual(["aaaaa bbbbb"], wrapped) #& white
if __name__ == '__main__':
unittest.main()
Overwriting testwrap.py
Run it again. It should give precise information if any test fails.
!python -munittest testwrap
.F ====================================================================== FAIL: test_no_wrapping_when_string_length_is_more_than_line_width (testwrap.TestForTextWrapping) ---------------------------------------------------------------------- Traceback (most recent call last): File "testwrap.py", line 14, in test_no_wrapping_when_string_length_is_more_than_line_width self.assertEqual(["aaaaa bbbbb"], wrapped) AssertionError: Lists differ: ['aaaaa bbbbb'] != ['aaaaa', 'bbbbb'] First differing element 0: aaaaa bbbbb aaaaa Second list contains 1 additional elements. First extra element 1: bbbbb - ['aaaaa bbbbb'] ? ^ + ['aaaaa', 'bbbbb'] ? ^^^^ ---------------------------------------------------------------------- Ran 2 tests in 0.001s FAILED (failures=1)
Which of the following does not need to be automated in a unit test case?
Fix the test now.
%%file testwrap.py
import unittest
from textwrap import TextWrapper
class TestForTextWrapping(unittest.TestCase):
def test_no_wrapping_when_string_length_is_less_than_line_width(self):
wrapper = TextWrapper(width=10)
wrapped = wrapper.wrap("a" * 5)
self.assertEqual(["a" * 5], wrapped)
def test_text_should_wrap_when_string_length_is_more_than_line_width(self):
wrapper = TextWrapper(width=10)
wrapped = wrapper.wrap("aaaaa bbbbb")
self.assertEqual(["aaaaa", "bbbbb"], wrapped)
if __name__ == '__main__':
unittest.main()
Overwriting testwrap.py
!time python -munittest testwrap
.. ---------------------------------------------------------------------- Ran 2 tests in 0.001s OK real 0m0.050s user 0m0.031s sys 0m0.016s
You should be able to run hundreds of unit tests within seconds.
In order to keep a unit test fast, which of the following do you need to avoid?
If your unit tests:
How often could you execute your unit tests?
In the example above, we have only one test module. But quite often we will need more test modules. If you still want to run the unit test in the same way, you need to manually import
all the test modules into one module.
There are several ways to discover test cases automatically. I would recommend nosetest
at the moment. To install it:
pip install nose
or
easy_install nose
Then you can simply run:
!nosetests
.. ---------------------------------------------------------------------- Ran 2 tests in 0.001s OK
Test group can have two optional functions, setUp
and tearDown
. setUp
will be called before each test case. tearDown
will be called after each test case. tearDown
will be called independently from the successful (or not) test outcome. It will be called even when there is an un-captured exception during the test case.
Test setup and teardown are sometimes called test fixtures. They are meant to create specific conditions of the code under test for the test cases. But a much easier way to understand their purpose is to view them as remove code duplicates among the test cases in the same test group.
Our previous tests can be changed to:
%%file testwrap.py
import unittest
from textwrap import TextWrapper
class TestForTextWrapping(unittest.TestCase):
def setUp(self):
self.wrapper = TextWrapper(width=10)
def test_no_wrapping_when_string_length_is_less_than_line_width(self):
wrapped = self.wrapper.wrap("a" * 5)
self.assertEqual(["a" * 5], wrapped)
def test_text_should_wrap_when_string_length_is_more_than_line_width(self):
wrapped = self.wrapper.wrap("aaaaa bbbbb")
self.assertEqual(["aaaaa", "bbbbb"], wrapped)
if __name__ == '__main__':
unittest.main()
Overwriting testwrap.py
!nosetests
.. ---------------------------------------------------------------------- Ran 2 tests in 0.002s OK
Use test fixtures to get rid of duplicate code in the test cases.
Test cases using the same test fixtures should be grouped together.
A good pattern to follow in a unit test is "AAA": Arrange, Act and Assert.
If you can easily find this pattern in each of your test cases, your tests should be easy to understand, and they should be fairly specific.
import unittest
class TestGroupForTextWrapping(unittest.TestCase):
def test_should_have_no_wrapping_when_string_length_is_5_and_line_width_is_10(self):
# Arrange: Arrange all necessary preconditions and inputs.
wrapper = TextWrapper(width=10)
# Act: Act on the object or method under test.
wrapped = wrapper.wrap("a" * 5)
# Assert: Assert that the expected results have occurred.
self.assertEqual(["a" * 5], wrapped)
Similar to the AAA pattern, the BDD style uses three other keywords to specify each test case: Given, When and Then. (You can also use And as another keyword.)
Given The Text Wrapper's Width Defined As 10
And Using '-' As Word Connector
When The Wrapper Wrap Text Length is Less Than 10
Then The Text Should Not Be Wrapped
As you can see, "given-when-then" maps to "arrange-act-assert" pretty well. They both simply define a state transition of a Finite State Machine (FSM). You can find more on this in the Uncle Bob's article. Some differences:
In general, this is a good rule for each unit test case:
Each unit test case should be very limited in scope.
So that:
Which of the following will you rarely see in a unit test case?
if
statementsfor
loopsWhy?
unittest
is not "pythonic" enough. It is not fully compliant with the PEP8 convention. For example, setUp
and assertEqual
are not good class method names (probably should be setup
and assert_equal
).nose
has many other features. Check the online documentation. py.test
. It can also run unittest
and nose
style tests, and is very easy to use.