Module 2, Practical 2¶
In this practical we will learn how to test a Python program and how to measure its runtime and memory usage (i.e., compexity in time and space).
Testing¶
Testing a software product allows to check whether the written code actually yields the expected results. In particular, testing the execution of code is called dynamic testing.
In Python, we can use assert to quickly test that functions we have written behave as they should:
assert(condition)
condition must be True for program execution to continue without errors.False, an AssertionError exception will be raised.[1]:
def sumOfTwo(x, y):
return x + y
# this should return 8
sumOfTwo(3,5)
# check that the function is working properly
assert(sumOfTwo(3,5) == 8)
# what happens here?
assert(sumOfTwo(3,5) == 1)
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
Cell In[1], line 11
8 assert(sumOfTwo(3,5) == 8)
10 # what happens here?
---> 11 assert(sumOfTwo(3,5) == 1)
AssertionError:
[2]:
sumOfTwo(3, "baduser")
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[2], line 1
----> 1 sumOfTwo(3, "baduser")
Cell In[1], line 2, in sumOfTwo(x, y)
1 def sumOfTwo(x, y):
----> 2 return x + y
TypeError: unsupported operand type(s) for +: 'int' and 'str'
Unit testing¶
Unit testing refers to tests that verify the functionality of a code unit, typically a function. Asserts are a quick way to test a piece of code but they don’t provide us with much detail (e.g. how different is the result we obtained from the expected one?). Luckily, Python provides us with a specific module for unit testing: unittest.
unittest is an OOP-based module - you need to subclass the unittest.TestCase class:
[3]:
import unittest
def fibonacci(n):
lst = []
for i in range(0, n):
if(i <= 1):
lst.append(1)
else:
lst.append(lst[i-2] + lst[i-1])
return(lst)
print("Fibonacci(5):", fibonacci(5))
class FibonacciTest(unittest.TestCase):
def test_listLength(self):
self.assertEqual(len(fibonacci(4)), 4)
Fibonacci(5): [1, 1, 2, 3, 5]
The test methods name begins with test_.
unittest offers many types of assertions: assertTrue, assertEqual, assertListEquals and more.
In a command-line environment, the unit test woud be launched by typing: python3 -m unittest file_test
Optional, not needed here: If you are in a notebook, we need to invoke the module explicitly: unittest.main(argv=[''], verbosity=2, exit=False)
Example
Try yourself to complete the FibonacciTest class by adding the following asserts:
check that the last (highest) number is correct
check that a negative
nprovided as input does not break the functioncheck that a floating point
nas input does not break the function
Show/Hide Solution
The checkFloatingPoint test fails. Let’s fix the fibonacci function to solve the issue and run the unit tests again!
What if we decide to not accept a floating point input? We can raise a TypeError exception and modify the unit test accordingly.
For a preliminary guide to Python exceptions, please refer to the official Python tutorial: https://docs.python.org/3/tutorial/errors.html
[4]:
import unittest
def fibonacci(n):
if isinstance(n, int):
lst = []
for i in range(0, n):
if(i <= 1):
lst.append(1)
else:
lst.append(lst[i-2] + lst[i-1])
return(lst)
else:
raise(TypeError) # we raise an exception!
print("Fibonacci(5):", fibonacci(5))
class FibonacciTest(unittest.TestCase):
def test_listLength(self):
self.assertEqual(len(fibonacci(4)), 4) # fibonacci(4)) returns 4 numbers
def test_checkLast(self):
self.assertEqual(max(fibonacci(5)), 5) # last number of fibonacci(5)) is 5
def test_checkNegative(self):
self.assertIsNotNone(fibonacci(-10)) # fibonacci(-10) returns something
def test_checkFloatingPoint(self):
with self.assertRaises(TypeError): # fibonacci(4.5) has to raise an exception
fibonacci(4.5)
Fibonacci(5): [1, 1, 2, 3, 5]
Exercise¶
Modify the FibonacciTest class to:
raise a
ValueErrorexception if the providednis negativecheck that a big
n(>>100, for example 100,000) does not raise aMemoryErrorexception
Show/Hide Solution
Measuring time¶
Another aspect of testing is to probe the time and memory required by your algorithms to be executed under different inputs and parameters.
timeit package to measure the execution time of small snippets of code.number parameter) to allow obtaining an estimate also for the quickest snippets :[8]:
import pandas as pd
import matplotlib.pyplot as plt
import timeit
def fibonacci(n):
if isinstance(n, int):
if n > 0:
lst = []
for i in range(0, n):
if(i <= 1):
lst.append(1)
else:
lst.append(lst[i-2] + lst[i-1])
return(lst)
else:
raise(ValueError)
else:
raise(TypeError)
# setup ensures that timeit can access specific functions from your local environment
timeit.timeit('fibonacci(5)', number=10000, setup="from __main__ import fibonacci")
times = []
times.append(timeit.timeit('fibonacci(5)', number=10000, setup="from __main__ import fibonacci"))
times.append(timeit.timeit('fibonacci(10)', number=10000, setup="from __main__ import fibonacci"))
times.append(timeit.timeit('fibonacci(15)', number=10000, setup="from __main__ import fibonacci"))
timeSeries = pd.Series(times)
timeSeries.plot()
# to change x labels...
plt.xticks([0, 1, 2], ['fibonacci(5)', 'fibonacci(10)', 'fibonacci(15)'])
plt.show()
plt.close()

Exercise¶
recursiveFibonacci function that computes the n-th Fibonacci number recursively.n ranging from 1 to 20 and plot the two distributionsShow/Hide Solution
Measuring memory¶
Memory used by an object, function or full Python scripts can be measured by different means.
The sys.getsizeof method is one of them:
[10]:
import sys
l1 = [1]*200
l2 = [2]*400
print(sys.getsizeof(l1))
print(sys.getsizeof(l2))
1656
3256
memory-profiler package.You can install it with pip: pip install -U memory_profiler
[ ]:
# Save this file as memProfSample.py
from memory_profiler import profile
@profile
def allocatingFunction():
a = [1] * (10 ** 6)
b = [2] * (2 * 10 ** 7)
del b
return a
if __name__ == '__main__':
allocatingFunction()
[ ]:
Filename: memProfSample.py
Line # Mem usage Increment Occurences Line Contents
============================================================
5 39.9 MiB 39.9 MiB 1 @profile
6 def allocatingFunction():
7 47.5 MiB 7.6 MiB 1 a = [1] * (10 ** 6)
8 200.1 MiB 152.6 MiB 1 b = [2] * (2 * 10 ** 7)
9 47.5 MiB -152.6 MiB 1 del b
10 47.5 MiB 0.0 MiB 1 return a
Exercise¶
Test the functionalities introduced in this practical to analyze some of the programs developed during the past lessons.
