Testing Lab
Objectives
The goal of today's activity is to practice working with assertions and unit testing in Python.
Part 1: Assertions
Assertions are used to verify that internal state of a program matches the programmer's expectations. Often, they are used to enforce preconditions for a function or snippet of code.
In Python, use the keyword assert
and a boolean condition to
check that condition at runtime. If the condition is false at the point it is
checked, an AssertionError
is raised. Here is an example
demonstrating the use of assert
in the Python interactive
interpreter:
Python 3.4.1 (default, Aug 13 2014, 16:11:43) [GCC 4.4.7 20120313 (Red Hat 4.4.7-4)] on linux Type "help", "copyright", "credits" or "license" for more information. >>> assert True >>> assert False Traceback (most recent call last): File "<stdin>", line 1, in <module> AssertionError >>> assert 4 < 5 >>> assert 5 < 4 Traceback (most recent call last): File "<stdin>", line 1, in <module> AssertionError >>> assert 3 + 5 == 8 >>> assert 3 + 5 == 9 Traceback (most recent call last): File "<stdin>", line 1, in <module> AssertionError
Often while you are writing code it is convenient to make assumptions about
certain conditions in your program, such as a function parameter's data type.
For example, a sqrt()
function might assume that its parameter is a
positive number. Such conditions may be true except in very rare circumstances
(such as a bug in the code), so it doesn't make sense to write full-fledged
error handling for the situation where the conditions are false. However, it is
still good practice to document these assumptions, either in a comment or in an
assertion. Here is an example:
def factorial(x): """ Returns the factorial of a positive integer. """ assert isinstance(x, int) assert x > 0 result = 1 for i in range(1, x+1): result *= i return result
In the above code, the assertions at the beginning of the
factorial()
function will cause an exception to be raised if the
parameter x
is not a positive integer.
TASK: Download assertions.py
and open it in an
editor. This file contains several functions and a short main()
.
Each function has a docstring describing the behavior of the function and the
desired properties of its parameters. Add assertions at the beginning of each
function to enforce those documented requirements. You should also add some
faulty calls to those methods in main()
to test your assertions.
HINT: Recall that you can test an object's type using the
isinstance()
built-in function, like this:
if isinstance(x, int): print("x is an integer!")
It is important not to overuse assertions. Python's dynamic type system raises automatic type errors when an object is used in a way that is not supported by its type, and it is not always useful to enforce type restrictions using assertions. In your code, you should weigh the benefits of the clarify provided by assertions against the loss of generality that they cause. For more information and some guidelines about the use of assertions in Python, see this wiki page.
However, for the purposes of this lab (to practice writing assertions), you should include assertions to enforce all of the restrictions included in the docstrings.
Part 2: Unit Testing
"Unit testing" refers to the practice of using tests to verify the behavior
of individual code modules. This is distinguished from end-to-end testing of a
larger piece of software, which is called "system" or "integration" testing.
While all types of testing are important, Python provides a very simple way to
write unit tests for your code, using the unittest
module.
Here is an example of unit tests, taken from the blog post in today's reading:
import unittest from primes import is_prime class PrimesTestCase(unittest.TestCase): """Tests for `primes.py`.""" def test_is_five_prime(self): """Is five successfully determined to be prime?""" self.assertTrue(is_prime(5)) if __name__ == '__main__': unittest.main()
The unittest
module provides a main()
function
that looks for classes that inherit from unittest.TestCase
, runs
their setUp()
method to initialize, then executes any methods whose
names begin with test_
. These methods should include calls to the
assertion methods that the class inherits from TestCase
. When the
test suite is finished executing, the framework will report the number of tests
run and the number that passed, as well as details on the ones that failed.
Here is a quick reference on the most useful assertions provided by
TestCase
:
Method | Condition |
---|---|
assertEqual(a, b) |
a == b |
assertTrue(x) |
bool(x) is True |
assertIn(a, b) |
a in b |
assertIsInstance(a, b) |
isinstance(a, b) |
In this lab, you will use the unittest
framework to
test implementations of the format_str
function.
To begin, download testing_files.zip
and unzip
it into a folder on your computer. This zip file contains
correct.py
, which is a reference implementation of
format_str
from Lab 2 and Homework 1. We have also included several
implementations (wrong##.py
) that contain bugs.
We have also included two other files, one of which you will need to modify
for this lab. Open tests.py
in an editor and read it carefully. It
contains a framework for testing the format_str
function
implementations. Currently it only includes a single unit test; you will need to
add more.
class TestFormatStr(unittest.TestCase): """ This class contains unit test cases for format_str """ def test_format0(self): """ Very basic test (just passthrough--no substitutions) """ self.assertEqual(candidate.format_str("test", []), "test")
The single unit test is the method test_format0
, which checks
to make sure that the result of calling format_str("test", [])
is
the string "test"
.
The other file is run.py
, which is a helper script that runs
the tests in tests.py
on all of the candidate code files
(correct.py
and wrong##.py
). It will print results
from each test run in sequence, starting with the correct version and then the
incorrect versions. The script does this by creating for each test a temporary
file candidate.py
containing the contents of the file to test.
Note that tests.py
imports candidate
. You shouldn't
need to modify this file.
Here is example output from executing run.py
:
$ python3 run.py ===== TESTING correct.py ===== . ---------------------------------------------------------------------- Ran 1 test in 0.000s OK ===== TESTING wrong01.py ===== test F ====================================================================== FAIL: test_format0 (__main__.TestFormatStr) Very basic test (just passthrough--no substitutions) ---------------------------------------------------------------------- Traceback (most recent call last): File "tests.py", line 26, in test_format0 self.assertEqual(candidate.format_str("test", []), "test") AssertionError: None != 'test' ---------------------------------------------------------------------- Ran 1 test in 0.000s FAILED (failures=1) ===== TESTING wrong02.py ===== . ---------------------------------------------------------------------- Ran 1 test in 0.000s OK ===== TESTING wrong03.py ===== . ---------------------------------------------------------------------- Ran 1 test in 0.000s OK ===== TESTING wrong04.py ===== . ---------------------------------------------------------------------- Ran 1 test in 0.000s OK TOTAL FAILED 1/5
As you can see, correct.py
passes the tests ("OK") but
wrong01.py
does not ("FAILED"). Check wrong01.py
to
make sure you understand why it does not pass the test in tests.py
.
Note that the test suite in tests.py
is currently incomplete,
because some of the incorrect versions pass the testing.
If run.py
does not work on your computer, you can still
complete the lab. Re-enable the commented-out lines (one at a time) in
tests.py
to import the individual candidate files. It will be
slower this way, however, because you have to manually run the tests for each
file.
TASK: Modify
tests.py
, adding tests until all of the incorrect versions fail at
least one of the tests. Make sure that the correct implementation still passes
all the tests (i.e., make sure that your tests are not overly restrictive). The
final output for a correct test suite end with the following:
TOTAL FAILED 4/5
You may find it helpful to use the diff
utility to spot
differences between the format_str
implementations. Example:
$ diff -u correct.py wrong01.py --- correct.py 2014-09-14 13:51:03.222753277 -0400 +++ wrong01.py 2014-09-14 13:51:03.222753277 -0400 @@ -1,5 +1,5 @@ """ -HW01: Basic Python (Correct Solution) +HW01: Basic Python Mike Lam August 2014 @@ -24,7 +24,7 @@ new_str += str(values[index]) # replacement value else: new_str += word # original text - return new_str + print(new_str) if __name__ == "__main__": pass
This makes it easy to see that the bug in wrong01.py
is that it
prints the result instead of returning it. That explains the
"AssertionError: None != 'test'
" message from the output of
run.py
. Figuring out the bug in each wrong version of the code may
help you to write appropriate unit tests. You could also use some of the example
test cases from the homework assignment page.
Submission
This lab will not be graded so there is nothing to submit. However, this lab may form the basis of a future homework assignment, and we may ask you to submit unit tests with future programming assignments. Make sure you keep a copy of your code for future reference. If you would like to discuss your solution or any problems you encounter while working on this lab, please come to office hours or make an appointment.