- assert statement
- Using assert to test a program
- Using unittest framework
- Using unittest.mock to test user input and program output
- Other testing frameworks
assert statement
assertis primarily used for debugging purposes like catching invalid input or a condition that shouldn't occur- An optional message can be passed for descriptive error message than a plain AssertionError
- It uses raise statement for implementation
assertstatements can be skipped by passing the-Ocommand line option- Note that
assertis a statement and not a function
>>> assert 2 ** 3 == 8 >>> assert 3 > 4 Traceback (most recent call last): File "<stdin>", line 1, in <module> AssertionError >>> assert 3 > 4, "3 is not greater than 4" Traceback (most recent call last): File "<stdin>", line 1, in <module> AssertionError: 3 is not greater than 4
Let's take a factorial function as an example:
>>> def fact(n): total = 1 for num in range(1, n+1): total *= num return total >>> assert fact(4) == 24 >>> assert fact(0) == 1 >>> fact(5) 120 >>> fact(-3) 1 >>> fact(2.3) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 3, in fact TypeError: 'float' object cannot be interpreted as an integer
assert fact(4) == 24andassert fact(0) == 1can be considered as sample tests to check the function
Let's see how assert can be used to validate arguments passed to the function:
>>> def fact(n): assert type(n) == int and n >= 0, "Number should be zero or positive integer" total = 1 for num in range(1, n+1): total *= num return total >>> fact(5) 120 >>> fact(-3) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in fact AssertionError: Number should be zero or positive integer >>> fact(2.3) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in fact AssertionError: Number should be zero or positive integer
The above factorial function can also be written using reduce
>>> def fact(n): assert type(n) == int and n >= 0, "Number should be zero or positive integer" from functools import reduce from operator import mul return reduce(mul, range(1, n+1), 1) >>> fact(23) 25852016738884976640000
Above examples for demonstration only, for practical purposes use math.factorial which also gives appropriate exceptions
>>> from math import factorial >>> factorial(10) 3628800 >>> factorial(-5) Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: factorial() not defined for negative values >>> factorial(3.14) Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: factorial() only accepts integral values
Further Reading
Using assert to test a program
In a limited fashion, one can use assert to test a program - either within the program (and later skipped using the -O option) or as separate test program(s)
Let's try testing the palindrome program we saw in Docstrings chapter
#!/usr/bin/python3 import palindrome assert palindrome.is_palindrome('Madam') assert palindrome.is_palindrome("Dammit, I'm mad!") assert not palindrome.is_palindrome('aaa') assert palindrome.is_palindrome('Malayalam') try: assert palindrome.is_palindrome('as2') except ValueError as e: assert str(e) == 'Characters other than alphabets and punctuations' try: assert palindrome.is_palindrome("a'a") except ValueError as e: assert str(e) == 'Less than 3 alphabets' print('All tests passed')
- There are four different cases tested for is_palindrome function
- Valid palindrome string
- Invalid palindrome string
- Invalid characters in string
- Insufficient characters in string
- Both the program being tested and program to test are in same directory
- To test the main function, we need to simulate user input. For this and other useful features, test frameworks come in handy
$ ./test_palindrome.py
All tests passed
Using unittest framework
This section requires understanding of classes
#!/usr/bin/python3 import palindrome import unittest class TestPalindrome(unittest.TestCase): def test_valid(self): # check valid input strings self.assertTrue(palindrome.is_palindrome('kek')) self.assertTrue(palindrome.is_palindrome("Dammit, I'm mad!")) self.assertFalse(palindrome.is_palindrome('zzz')) self.assertFalse(palindrome.is_palindrome('cool')) def test_error(self): # check only the exception raised with self.assertRaises(ValueError): palindrome.is_palindrome('abc123') with self.assertRaises(TypeError): palindrome.is_palindrome(7) # check error message as well with self.assertRaises(ValueError) as cm: palindrome.is_palindrome('on 2 no') em = str(cm.exception) self.assertEqual(em, 'Characters other than alphabets and punctuations') with self.assertRaises(ValueError) as cm: palindrome.is_palindrome('to') em = str(cm.exception) self.assertEqual(em, 'Less than 3 alphabets') if __name__ == '__main__': unittest.main()
- First we create a subclass of unittest.TestCase (inheritance)
- Then, different type of checks can be grouped in separate functions - function names starting with test are automatically called by unittest.main()
- Depending upon type of test performed, assertTrue, assertFalse, assertRaises, assertEqual, etc can be used
- An Introduction to Classes and Inheritance
- Python docs - unittest
$ ./unittest_palindrome.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
$ ./unittest_palindrome.py -v
test_error (__main__.TestPalindrome) ... ok
test_valid (__main__.TestPalindrome) ... ok
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
Using unittest.mock to test user input and program output
This section requires understanding of decorators, do check out this wonderful intro
A simple example to see how to capture print output for testing
>>> from unittest import mock >>> from io import StringIO >>> def greeting(): print('Hi there!') >>> def test(): with mock.patch('sys.stdout', new_callable=StringIO) as mock_stdout: greeting() assert mock_stdout.getvalue() == 'Hi there!\n' >>> test()
One can also use decorators
>>> @mock.patch('sys.stdout', new_callable=StringIO) def test(mock_stdout): greeting() assert mock_stdout.getvalue() == 'Hi there!\n'
Now let's see how to emulate input
>>> def greeting(): name = input('Enter your name: ') print('Hello', name) >>> greeting() Enter your name: learnbyexample Hello learnbyexample >>> with mock.patch('builtins.input', return_value='Tom'): greeting() Hello Tom
Combining both
>>> @mock.patch('sys.stdout', new_callable=StringIO) def test_greeting(name, mock_stdout): with mock.patch('builtins.input', return_value=name): greeting() assert mock_stdout.getvalue() == 'Hello ' + name + '\n' >>> test_greeting('Jo')
Having seen basic input/output testing, let's apply it to main function of palindrome
#!/usr/bin/python3 import palindrome import unittest from unittest import mock from io import StringIO class TestPalindrome(unittest.TestCase): @mock.patch('sys.stdout', new_callable=StringIO) def main_op(self, tst_str, mock_stdout): with mock.patch('builtins.input', side_effect=tst_str): palindrome.main() return mock_stdout.getvalue() def test_valid(self): for s in ('Malayalam', 'kek'): self.assertEqual(self.main_op([s]), s + ' is a palindrome\n') for s in ('zzz', 'cool'): self.assertEqual(self.main_op([s]), s + ' is NOT a palindrome\n') def test_error(self): em1 = 'Error: Characters other than alphabets and punctuations\n' em2 = 'Error: Less than 3 alphabets\n' tst1 = em1 + 'Madam is a palindrome\n' self.assertEqual(self.main_op(['123', 'Madam']), tst1) tst2 = em2 + em1 + 'Jerry is NOT a palindrome\n' self.assertEqual(self.main_op(['to', 'a2a', 'Jerry']), tst2) if __name__ == '__main__': unittest.main()
- Two test functions - one for testing valid input strings and another to check error messages
- Here, side_effect which accepts iterable like list, compared to return_value where only one input value can be mocked
- For valid input strings, the palindrome main function would need only one input value
- For error conditions, the iterable comes handy as the main function is programmed to run infinitely until valid input is given
- Python docs - unittest.mock
- An Introduction to Mocking in Python
- PEP 0318 - decorators
- decorators
$ ./unittest_palindrome_main.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.003s
OK
Other testing frameworks
- pytest
- Python docs - doctest
- Python test automation
- Python Testing Tools Taxonomy
- Python test frameworks
Test driven development (TDD)