A gist illustrating testing in Python using unittest
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# encoding: utf-8 | |
""" | |
Some exercising of Python test functionality based on: | |
https://docs.python.org/3/library/doctest.html | |
https://docs.python.org/3/library/unittest.html | |
Generating tests dynamically with unittest | |
https://eli.thegreenplace.net/2014/04/02/dynamically-generating-python-test-cases | |
Supressing log output to console: | |
https://stackoverflow.com/questions/2266646/how-to-disable-and-re-enable-console-logging-in-python | |
The tests in this file are run using: | |
./tests.py -v | |
Ian Hopkinson 2020-11-18 | |
""" | |
import unittest | |
import logging | |
def factorial(n): | |
"""Return the factorial of n, an exact integer >= 0. | |
>>> [factorial(n) for n in range(6)] | |
[1, 1, 2, 6, 24, 120] | |
>>> factorial(30) | |
265252859812191058636308480000000 | |
>>> factorial(-1) | |
Traceback (most recent call last): | |
... | |
ValueError: n must be >= 0 | |
Factorials of floats are OK, but the float must be an exact integer: | |
>>> factorial(30.1) | |
Traceback (most recent call last): | |
... | |
ValueError: n must be exact integer | |
>>> factorial(30.0) | |
265252859812191058636308480000000 | |
It must also not be ridiculously large: | |
>>> factorial(1e100) | |
Traceback (most recent call last): | |
... | |
OverflowError: n too large | |
""" | |
import math | |
if not n >= 0: | |
raise ValueError("n must be >= 0") | |
if math.floor(n) != n: | |
raise ValueError("n must be exact integer") | |
if n+1 == n: # catch a value like 1e300 | |
raise OverflowError("n too large") | |
result = 1 | |
factor = 2 | |
while factor <= n: | |
result *= factor | |
factor += 1 | |
return result | |
class TestFactorial(unittest.TestCase): | |
test_cases = [(0, 1, "zero"), | |
(1, 1, "one"), | |
(2, 2, "two"), | |
(3, 5, "three"), # should be 6 | |
(4, 24, "four"), # should be 24 | |
(5, 120, "five")] | |
def test_that_factorial_30(self): | |
self.assertEqual(factorial(30), 265252859812191058636308480000000) | |
def test_that_factorial_argument_is_positive(self): | |
with self.assertRaises(ValueError): | |
factorial(-1) | |
def test_that_a_list_of_factorials_is_calculated_correctly(self): | |
# nosetests does not run subTests correctly: | |
# It does not report which case fails, and stops on failure | |
for test_case in self.test_cases: | |
with self.subTest(msg=test_case[2]): | |
print("Running test {}".format(test_case[2]), flush=True) | |
logging.info("Running test {}".format(test_case[2])) | |
self.assertEqual(factorial(test_case[0]), test_case[1], "Failure on {}".format(test_case[2])) | |
@unittest.skip("demonstrating skipping") | |
def test_nothing(self): | |
self.fail("shouldn't happen") | |
if __name__ == '__main__': | |
# Doctests will run if they are invoked before unittest but not vice versa | |
# nosetest will only invoke the unittests by default | |
import doctest | |
doctest.testmod() | |
# If you want your generated tests to be separate named tests then do this | |
# This is from https://eli.thegreenplace.net/2014/04/02/dynamically-generating-python-test-cases | |
def make_test_function(description, a, b): | |
def test(self): | |
self.assertEqual(factorial(a), b, description) | |
return test | |
testsmap = { | |
'test_one_factorial': [1, 1], | |
'test_two_factorial': [2, 3], | |
'test_three_factorial': [3, 6]} | |
for name, params in testsmap.items(): | |
test_func = make_test_function(name, params[0], params[1]) | |
setattr(TestFactorial, 'test_{0}'.format(name), test_func) | |
# This supresses logging output to console, like the --nologcapture flag in nosetests | |
logging.getLogger().addHandler(logging.NullHandler()) | |
logging.getLogger().propagate = False | |
# Finally we run the tests | |
unittest.main(buffer=True) # supresses print output, like --nocapture in nosetests or you can use -b | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment