Joseph P. Vantassel, jpvantassel.com
Unit testing is the automated process of checking the result of a small snippet of code with known inputs to ensure they produce some known output.
The snippet of code may be a function or method, but must it must at a minimum have a clear input and known output. The known output may be the result of a hand-calculation or a published solution from a reputable source. If using a published solution, be sure to include a reference for posterity. In all cases unit tests are written once (and only once) programmatically so that they can be performed quickly and consistently.
Various frameworks, among the most popular:
unittestpytesthypothesis
In this module we will discuss the unittest framework.
unittest is defined as an object oriented testing framework.
unittest has four main components:
- test fixture : A test fixture represents the preparation and cleanup for testing.
- test case: A test case is the individual unit of testing.
- test suite: A test suite is a collection of test cases, test suites, or both. It is used to aggregate related tests and suites.
- test runner: A test runner is a component which orchestrates the execution of tests.
The good news is that writing a unit test is easy and straightforward. And odds are that, even if you are not currently using a unit test framework, you are probably already "doing the hard part" by checking your code in some adhoc manner. All you need to do is to write these adhoc tests down programmatically so that they can be repeated quickly and precisely.
The challenging part of unit testing is selecting/developing test cases that are significant. You may think that all of your code is significant and you should have a test for every part of your code, and while this is a great goal it is often impossible/impractical at a projects outset. Focus on writing unit tests for the parts of your code which are fragile (i.e., most easily broken during refactoring), frequently called (e.g., methods in a base class), or have been the cause of previous bugs. Once you have these trouble areas covered then work to get to 100% test coverage, just remember that 100% test coverage does not guarantee that your code is correct.
The best time to write a unit test is at the time you write the corresponding code. You will already be familiar with what the code is doing and should have a good feeling for things that could go wrong.
The second best time to write a unit test is after you have just found and fixed a bug. You spent the time to find the bug, so use the reintroduction to your advantage by writing a test that will prevent this bug from re-emerging in the future.
There are two main schools of thought.
-
At the bottom of the module it is designed to test.
Pros
- Tests and code are inside a single file, which is convenient for non-packaged modules.
- Do not need to change directories to run the test, by running the module you will automatically run its tests.
-
In a separate folder called
testwith a new file for each module.Pros
- The test module can be run separately from the command line.
- The test code can more easily be separated from shipped code.
- There is less temptation to change the test, rather than fixing the code.
- Test code should be modified less frequently than the code it tests.
- Tests can be refactored more easily.
- If the testing strategy changes then there is no reasons to change the source.
In short, if you are developing a module or package that is unlikely
to be shared, writing your tests in the same file offers some advantages,
however placing your tests in a test folder with one test file per module is a
more general and extensible option and is generally recommended.
Lets take a very simple example where we have a function called add which
surprisingly enough adds two numbers, and let's verify that is working
correctly.
import unittest # Import framework
def add(a, b): # Standard python function
"""Add a and b and return the result."""
return a+b
class Test_Math(unittest.TestCase): # Define test suite. Note that
# we are inheriting from TestCase
def test_add(self): # Define a test case
a = 2
b = 2
self.assertEqual(add(a,b), a+b) # Call runner
if __name__ == '__main__': # Return result of test, if main
unittest.main()Which results in the following.
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OKAnd here is a more complicated example, where we want to examine some of the
built-in methods for str type variables.
import unittest # Import framework
class TestStringMethods(unittest.TestCase): # Define test suite
def test_upper(self): # Define a test case
expected = 'FOO'
returned = 'foo'.upper()
self.assertEqual(expected, returned) # Call runner
def test_isupper(self): # Define another test case
self.assertTrue('FOO'.isupper())
self.assertFalse('Foo'.isupper())
def test_split(self): # And another test case
s = 'hello world'
self.assertEqual(s.split(), ['hello', 'world'])
with self.assertRaises(TypeError):
s.split(2)
if __name__ == '__main__': # Return result of test, if main
unittest.main()Which results in the following.
...
----------------------------------------------------------------------
Ran 3 tests in 0.007s
OKDefining the methods setUp() and tearDown() in your test suite allows for
repetitive preparation and clean-up operations to be defined only once for all
of the test cases in the suite.
setUp(): Performs any repetitive pre-test work. Runs automatically before each test case.tearDown(): Performs any repetitive post-test clean-up. Run automatically after each test case, regardless of whether they were successful.
import unittest # Import framework
class TestExample(unittest.TestCase): # Define test suite
def setUp(self): # Define setUp (done first)
print("Hello from setup()")
def teatDown(self): # Define tearDown (done last)
print("Goodbye from tearDown()")
def test_add(self):
expected = 5
returned = 2 + 3
self.assertEqual(expected, returned) # Will pass
def test_substract(self):
expected = 3
returned = 5 - 1
self.assertEqual(expected, returned) # Will fail
if __name__ == '__main__': # Return result of test, if main
unittest.main()Which results in the following.
Hello from setup()
Goodbye from tearDown()
.Hello from setup()
Goodbye from tearDown()
F
======================================================================
FAIL: test_substract (__main__.TestExample)
----------------------------------------------------------------------
Traceback (most recent call last):
File "c/example.py", line 19, in test_substract
self.assertEqual(expected, returned)
AssertionError: 3 != 4
----------------------------------------------------------------------
Ran 2 tests in 0.012s
FAILED (failures=1)Note that Hello from setup() and Goodbye from tearDown() both appear twice,
once for each of our test cases even though one of the test cases failed.