Python unittest
Assumed Knowledge
- Variables and Operators in Python
- Control structures (conditions and loops)
- Functions
Learning Outcomes
- Be acquainted with unittest environment.
- Write a piece of code based on tests provided.
Author: Gaurav Gupta
Introduction
It is common to write a function and make a simple mistake that does not reveal itself until it’s too late.
Example 1
1
2
3
4
5
def is_positive(val):
if val >= 0:
return True
else:
return False
This function would return true for for any val
that is greater than or equal to zero. However, zero is NOT a positive number. Neither is it a negative number.
Wouldn’t it be greate, if we could catch this issue nice and early?
That is what unit testing provides. A set of inputs and corresponding expected outputs.
In this case, the following would be a start but not an adequate set.
val | value returned |
---|---|
1.4 | True |
-2.7 | False |
Edge cases should also be considered. So, we will include values on either side of zero, and zero itself.
val | value returned |
---|---|
1.4 | True |
-2.7 | False |
-0.0001 | False |
0 | False |
0.0001 | True |
Write tests before code
In test-driven development, tests are written before the code and cover the different scenarios possible.
Example 2
Problem definition: Write a function that takes in three numbers, and returns the median value, that is the value that sits in the middle when the numbers are arranged in ascending (or descending) order.
For number 1.5, 0.7 and 8.3, it’s the number 1.5 that is the median value.
Let us write some tests and compare our notes.
First family of values will be three distinct values.
The median should lie in all three places across the set.
- Second value is the median
- 1.2, 3.5, 8.4: First less than third
- 8.4, 3.5, 1.2: First more than third
- First value is the median
- 3.5, 1.2, 8.4: Second less than third
- 3.5, 8.4, 1.2: Second more than third
- Third value is the median
- 8.4, 1.2, 3.5: First more than second
- 1.2, 8.4, 3.5: First less than second
Second family of values should be two of the same values and a third unique value.
The order should be shuffled to explore all combinations. On the same line of thought as the first family, we get the following combinations
- First and second are the same
- 25, 25, 75: Unique value is higher
- 75, 75, 25: Unique value is lower
- First and third are the same
- 25, 75, 25: Unique value is higher
- 75, 25, 75: Unique value is lower
- Second and third are the same
- 75, 25, 25: Unique value is higher
- 25, 75, 75: Unique value is lower
Note that we should try and not re-use the same few numerical value for all the cases, hence we replaced 1.2, 3.5 and 8.4 with 25 and 75 for the second family.
Third family of values is where all three values are the same.
- -10000, -10000, -10000
The complete set, and required answer, are:
Input | Output |
---|---|
1.2, 3.5, 8.4 | 3.5 |
8.4, 3.5, 1.2 | 3.5 |
3.5, 1.2, 8.4 | 3.5 |
3.5, 8.4, 1.2 | 3.5 |
8.4, 1.2, 3.5 | 3.5 |
1.2, 8.4, 3.5 | 3.5 |
25, 25, 75 | 25 |
75, 75, 25 | 75 |
25, 75, 25 | 25 |
75, 25, 75 | 75 |
75, 25, 25 | 25 |
25, 75, 75 | 75 |
-10000, -10000, -10000 | -10000 |
Assertions
Python checks if the input matches the value returned using assertions.
assertEqual
The simplest assertion is assertEqual
. The following assertion passes if and only if a
is exactly the same as b
.
1
assertEqual(a, b)
Example:
1
assertEqual(square(6), 36)
assertAlmostEqual
assertAlmostEqual
takes into account rounding-off errors that may occur while comparing floating-point values. The following assertion passes if and only if a
is almost the same as b
, upto 6 to 7 decimal places by default, or up to n
decimal places if the third parameter is supplied. If n
is supplied, it rounds off the numbers to n
decimal places. 2.236
rounded-off to 2 decimal places will be 2.24
, while 1.4641
rounded-off to 2 decimal places will be 1.46
.
1
2
3
assertAlmostEqual(a, b)
#OR
assertAlmostEqual(a, b, n)
Example:
1
2
assertAlmostEqual(square(1.25), 1.5625)
assertAlmostEqual(square(1.2), 1.464, 3) #just check first 3 decimal places
assertTrue, assertFalse
assertTrue
passes if and only if the value passed to the assertion is True
.
1
assertTrue(is_positive(3))
assertFalse
is the opposite, it passes if and only if the value passed to the assertion is False
.
1
assertFalse(isPositive(0))
Summary of assertions
A list of all assertions is given below:
Method | Checks | Version |
---|---|---|
assertEqual | a == b | 3.x |
assertNotEqual | a != b | 3.x |
assertTrue | bool(x) is True | 3.x |
assertFalse | bool(x) is False | 3.x |
assertIs | a is b | 3.x |
assertIsNot | a is not b | 3.x |
assertIsNone | x is None | 3.x |
assertIsNotNone | x is not None | 3.x |
assertIn | a in b | 3.x |
assertNotIn | a not in b | 3.x |
assertIsInstance | is instance(a,b) | 3.x |
assertNotIsInstance | not is instance(a,b) | 3.x |
assertRaises | fun(*args,**kwds) raises exc | 3.x |
assertRaisesRegexp | fun(*args,**kwds) raises exc(regex) | 3.x |
assertAlmostEqual | round(a-b,7) == 0 | 3.x |
assertNotAlmostEqual | round(a-b,7) != 0 | 3.x |
assertGreater | a > b | 3.x |
assertGreaterEqual | a >= b | 3.x |
assertLess | a < b | 3.x |
assertLessEqual | a <= b | 3.x |
assertRegexpMatches | r.search(s) | 3.x |
assertNotRegexpMatches | not r.search(s) | 3.x |
assertItemsEqual | sorted(a) == sorted(b) | 3.x |
assertDictContainsSubset | all the key/value pairs in a exist in b | 3.x |
assertMultiLineEqual | strings | 3.x |
assertSequenceEqual | sequences | 3.x |
assertListEqual | lists | 3.x |
assertTupleEqual | tuples | 3.x |
assertSetEqual | sets or frozensets | 3.x |
assertDictEqual | dicts | 3.x |
How are assertions used
First thing is to import the unittest
library as:
1
import unittest
Assertions need to be wrapped in a class with the header:
1
class Tester(unittest.TestCase):
and then inside a function with the header:
1
def test_function_name(self):
So, if the name of the function to be tested is is_positive
, it would become:
1
def test_is_positive(self):
and if the name of the function to be tested is square
, it would become:
1
def test_square(self):
So far, we got:
1
2
class Tester(unittest.TestCase):
def test_function_name(self):
In there, you can have whatever assertions you want. You just need to prefix each assertion with self.
. Some examples:
1
2
3
self.assertTrue(is_positive(20))
self.assertEqual(square(6), 36))
self.assertAlmostEqual(average(3, 4), 3.5)
One final step
The final step is to include the driver, so as to specify that it’s a unittest
we want to run.
1
2
if __name__ == "__main__":
unittest.main()
Outcomes
If all the tests pass, python tells you everything went well, using OK.
1
2
3
4
5
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
In case of failure, it will tell you the problem and also that it Failed. For example:
1
2
3
4
5
6
7
8
9
Traceback (most recent call last):
File "/Users/gauravgupta/Documents/SoftwareTechnologyMQ.github.io/assets/codes/unittest_example_1.py", line 5, in test_is_positive
self.assertTrue(is_positive(5635.5))
AssertionError: False is not true
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
If all tests in a file pass, you should see something like (example for file containing 13 tests):
1
2
3
4
5
.............
----------------------------------------------------------------------
Ran 13 tests in 0.023s
OK
For each test failed, you get an explanation, such as,
1
2
3
4
5
6
7
8
9
10
11
12
13
..........F..
======================================================================
FAIL: test_sum_even (__main__.Tester)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/gauravgupta/Downloads/comp6010practicePackage-main/02_loops.py", line 195, in test_sum_even
self.assertEqual(sum_even(2, 6), 6)
AssertionError: 8 != 6
----------------------------------------------------------------------
Ran 13 tests in 0.024s
FAILED (failures=1)
Here, it’s saying that self.assertTrue(is_positive(5635.5))
failed because the function call resulted in False
, which is not True
(as per the assertions demands).
First complete example (assertTrue, assertFalse)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import unittest
class Tester(unittest.TestCase):
def test_is_positive(self):
self.assertTrue(is_positive(5635.5))
self.assertTrue(is_positive(1))
self.assertTrue(is_positive(0.1))
self.assertTrue(is_positive(0.01))
self.assertFalse(is_positive(0))
self.assertFalse(is_positive(-0.1))
self.assertFalse(is_positive(-0.01))
self.assertFalse(is_positive(-11454.4))
def is_positive(val):
if val > 0:
return True
else:
return False
if __name__ == "__main__":
unittest.main()
Second complete example (assertEqual)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import unittest
class Tester(unittest.TestCase):
def test_highest(self):
self.assertEqual(3, highest(1, 1, 3))
self.assertEqual(7, highest(5, 7, 5))
self.assertEqual(-4, highest(-4, -10, -10))
self.assertEqual(1, highest(1, 1, -3))
self.assertEqual(5, highest(5, -7, 5))
self.assertEqual(-10, highest(-14, -10, -10))
self.assertEqual(1729, highest(1729, 1729, 1729))
self.assertEqual(30, highest(10, 20, 30))
self.assertEqual(100, highest(50, 100, 60))
self.assertEqual(-100, highest(-500, -120, -100))
def highest(a, b, c):
if a>=b:
if a>=c:
return a
else:
return c
else:
if b>=c:
return b
else:
return c
if __name__ == "__main__":
unittest.main()
Third complete example (assertAlmostEqual)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import unittest
class Tester(unittest.TestCase):
def test_average(self):
self.assertAlmostEqual(2.5, average(2, 3))
self.assertAlmostEqual(-2.5, average(-2, -3))
self.assertAlmostEqual(0, average(2, -2))
self.assertAlmostEqual(-51.3, average(-51.1, -51.5))
#up to 2 decimal places
self.assertAlmostEqual(average(1.2555, 1.3), 1.28, 2)
#up to 1 decimal place only
self.assertAlmostEqual(average(1.2345, 4.5678), 2.9, 1)
def average(a, b):
return (a+b)/2
if __name__ == "__main__":
unittest.main()
An example where you complete the function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import unittest
class Tester(unittest.TestCase):
def test_is_odd(self):
self.assertTrue(is_odd(5))
self.assertTrue(is_odd(-5))
self.assertTrue(is_odd(517))
self.assertTrue(is_odd(-6561))
self.assertFalse(is_odd(0))
self.assertFalse(is_odd(8))
self.assertFalse(is_odd(-8))
self.assertFalse(is_odd(8096))
self.assertFalse(is_odd(-8096))
def is_odd(val):
return True # replace this with actual code
if __name__ == "__main__":
unittest.main()
An example where you write the tests and then the function
1
2
3
4
5
6
7
8
9
10
11
import unittest
class Tester(unittest.TestCase):
def test_is_divisible_by(self):
# TODO
def is_odd(a, b):
return True #update so it returns True if and only if a is divisible by b
if __name__ == "__main__":
unittest.main()
PRACTICE PACKAGE!!!
Find the entire practice package at COMP6010PracticePackage.zip