Unit Testing#
Unit testing is a method of testing your code by writing tests for individual functions.
Let’s say you write a package which provides a function. You want to convince someone (espeically yourself) that the function works as intended. An excellent way to do this is to write unit tests, which (assuming they pass) demonstrate that your function does what is supposed to do, at least for the situations you test.
unittest package#
unittest is a built-in package which provides unit testing capabilities.
Generally, you define classes that inherit from unittest.TestCase
. Then you can add methods which test different functionality.
'foo'.upper().isupper()
True
import unittest
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')
def test_isupper(self):
self.assertTrue('ABC'.isupper())
self.assertFalse('Foo'.isupper())
def test_split(self):
s = 'hello world'
self.assertEqual(s.split(), ['hello', 'world'])
# check that s.split fails when the separator is not a string
with self.assertRaises(TypeError):
s.split(2)
We can then run our tests using unittest.main()
(the arguments below are passed in so we can run in Jupyter).
unittest.main(argv=['first-arg-is-ignored'], exit=False)
.
.
.
----------------------------------------------------------------------
Ran 3 tests in 0.003s
OK
<unittest.main.TestProgram at 0x7fdc7c294b90>
When using floating-point numbers, use assertAlmostEqual
instead of assertEqual
to test for numerical equality
1.2 - 1.0
0.19999999999999996
class TestFloatArithmetic(unittest.TestCase):
def test_approx(self):
self.assertAlmostEqual(1.2 - 1.0, 0.2)
def test_exact(self):
self.assertEqual(1.2 - 1.0, 0.2 )
unittest.main(argv=['first-arg-is-ignored'], exit=False)
.
F
.
.
.
======================================================================
FAIL: test_exact (__main__.TestFloatArithmetic.test_exact)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/ipykernel_1668/3868928738.py", line 7, in test_exact
self.assertEqual(1.2 - 1.0, 0.2 )
AssertionError: 0.19999999999999996 != 0.2
----------------------------------------------------------------------
Ran 5 tests in 0.005s
FAILED (failures=1)
<unittest.main.TestProgram at 0x7fdc7c297810>
Running from command line#
The more common way to run unit tests is to have them in a test folder or file test.py
. You can then run tests using
python -m unittest test.py
Or use pytest
via
pytest test.py
pytest is another Python testing framework - it is compatible with unittest
, and has additional funcitonality which we aren’t going to cover.
Test-Driven Development#
You don’t need to wait to implement everything in order to write your tests. Writing your tests first is called test-driven development. One advantage of test-driven development is that you’ll know when you have succeeded in your implementation, since all your tests will pass.
Let’s consider a suite of tests that would test a power_method
function:
import numpy as np
np.ones((5,5))
array([[1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1.]])
class TestPowerMethod(unittest.TestCase):
def test_eigenpair(self):
n = 5
A = np.random.randn((n,n))
A = A + A.T # make symmetric
lam, v = powermethod(A)
v2 = A @ v
self.assertAlmostEqual(np.linalg.norm(v2 - lam * v), 0)
def test_norm1(self):
n = 5
A = np.random.randn((n,n))
A = A + A.T # make symmetric
lam, v = powermethod(A)
self.assertAlmostEqual(np.linalg.norm(v), 1)
def test_rank1(self):
n = 5
A = np.ones((n,n))
lam, v = powermethod(A)
# check that v is close to constant function.
self.assertAlmostEqual(np.linalg.norm(v - np.ones(n)/np.sqrt(n)), 0)
Exercise#
Implement a function powermethod
which satisfies the above tests (using the Power method algorithm, of course)
## Your code here
Further Examples#
Check out
test.py
in each homework assignment, which is used for autograding.You can find another example in the repository
python-packages
Continuous Integration#
Continuous Integration, or CI, is the practice of automatically building code and running tests continuously. Continuously in this case generally means any time changes are made, which might be multiple times a day.
The advantage of CI is that when you make changes to your code, you quickly find out if there are problems that need to be solved if your tests fail. You can run these tests before merging branches in your git repository, making sure that checks pass.
GitHub actions is one way of implementing CI. This is what we are using in this class - you can find an example in the python-packages
repository.
Another popular option which you may see in open source software is Travis-CI.
Both these platforms are configured using a *.yml
file. See the python-packages
repository for an example.
You can do more than just run unit tests using CI, such as running integration tests, verifying that data analyses don’t change, etc.