Unit Testing¶
Since Github offerd a builtin unittesting/CI (continuous integration) service it is relatively easy make your code pull-request-safe. But first a little bit about testing itself.
Setting up your tests¶
The first thing you need to do is to set up some tests. As you may have seen in
The module, matpy
contains a subdirectory, which contains a
testing file, specifically the test file test_matmul_and_dot.py
:
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | #!/usr/bin/env python
"""
This is a small script that shows how to simply create a tests class. The
reason why a tests class is the superior choice over a function is that it can
set up a testing environment, e.g. a tests directory structure needed to check
existing files. The unittest testclass also contains an easy way to check
whether your function throws an error, when it should.
:author:
Lucas Sawade (lsawade@princeton.edu, 2019)
:license:
GNU Lesser General Public License, Version 3
(http://www.gnu.org/copyleft/lgpl.html)
"""
import unittest
import numpy as np
from matpy.matrixmultiplication import matmul
from matpy.matrixmultiplication import dotprod
from matpy.matrixmultiplication import MatrixMultiplication
class TestMatMul(unittest.TestCase):
""""A sample tests class to check if your modules' functions ar
functioning."""
def setUp(self):
"""
The setUp command is used to reduce the need for large amounts of
redudandant code. This will be executed and setup once before every
of your test-class' method.
Very useful if you want to load and setup a certain object.
"""
# This might seem a little fabricated and unecessary for our example
# here setUp is actually an overkill
self.a1 = [1, 0]
self.a2 = [0, 1]
self.b1 = [4, 1]
self.b2 = [2, 2]
def test_raise_shape_error(self):
"""Tests if error is raised when either A or B does not have 2
dimensions"""
# A does not have 2 dimensions
a = np.array([[self.a1, self.a2], [self.a2, self.a2]])
b = np.array([self.b1, self.b2])
with self.assertRaises(ValueError):
matmul(a, b)
# B does not have 2 dimensions
a = np.array([self.a1, self.a2])
b = np.array([[self.a1, self.a2], [self.a2, self.a2]])
with self.assertRaises(ValueError):
matmul(a, b)
def test_raise_shape_match_error(self):
"""Tests whether an error is thrown when b doesn't match a."""
# B has more rows than a has columns!
a = np.array([self.a1, self.a2])
b = np.array([self.b1, self.b2, self.b2])
# Check if error is raised
with self.assertRaises(ValueError):
matmul(a, b)
def test_multiplication(self):
"""Test the multiplication itself."""
# Define matrix content.
a = np.array([self.a1, self.a2])
b = np.array([self.b1, self.b2])
# Check result
self.assertTrue(np.all(np.array([self.b1, self.b2] == matmul(a, b))))
class TestDot(unittest.TestCase):
""""A sample tests class to check if your modules' functions ar
functioning."""
def test_raise_shape_error(self):
"""Tests if error is raised when either A or B does not have 2
dimensions"""
# A does not have 2 dimensions
a = np.array([[[1, 0], [0, 1]], [[0, 1], [0, 1]]])
b = np.array([[4, 1], [2, 2]])
with self.assertRaises(ValueError):
dotprod(a, b)
# B does not have 2 dimensions
a = np.array([[1, 0], [0, 1]])
b = np.array([[[1, 0], [0, 1]], [[0, 1], [0, 1]]])
with self.assertRaises(ValueError):
dotprod(a, b)
def test_raise_shape_match_error(self):
"""Tests whether an error is thrown when b doesn't match a."""
# B has more rows than a has columns!
a = np.array([[1, 0], [0, 1]])
b = np.array([[4, 1], [2, 2], [2, 2]])
# Check if error is raised
with self.assertRaises(ValueError):
dotprod(a, b)
def test_multiplication(self):
"""Test the multiplication itself."""
# Define matrix content.
a = np.array([[1, 0], [0, 1]])
b = np.array([[4, 1], [2, 2]])
# Check result
self.assertTrue(np.all(np.array([[4, 1], [2, 2]] == matmul(a, b))))
class TestMM(unittest.TestCase):
""""A sample tests class to check if your modules' functions are
functioning."""
def test_raise_method_error(self):
"""Tests if error is raised when either A or B does not have 2
dimensions, but here mainly for class initiation. As the methods
themselves are already proven to work."""
# A does not have 2 dimensions
a = np.array([[[1, 0], [0, 1]], [[0, 1], [0, 1]]])
b = np.array([[4, 1], [2, 2]])
# Assign wrong method to raise error
method = "blub"
with self.assertRaises(ValueError):
MM = MatrixMultiplication(a, b, method=method)
# Assign right method to check for size error
method = "matmul"
with self.assertRaises(ValueError):
MM = MatrixMultiplication(a, b, method=method)
MM()
# B does not have 2 dimensions
a = np.array([[1, 0], [0, 1]])
b = np.array([[[1, 0], [0, 1]], [[0, 1], [0, 1]]])
with self.assertRaises(ValueError):
MM = MatrixMultiplication(a, b, method=method)
print(MM())
if __name__ == "__main__":
unittest.main()
|
When inspecting the file you will, see
three different unittest classes – for each function and one for the class.
These are not perfect tests, but they do illustrate the basic usage of the
unittest.TestCase
, which is very useful especially if you want to
set up a test environment. Note that in the last test we are not testing the
output of the class’ run since it is already tested with the previous two tests.
This is the great thing about unit testing.
The TestMatMul
class’ first method is SetUp()
. This method is executed
when the class is initialised, so variables and some computations do not have to
run for each testing method. Instead those variables will be saved as attributes
of the test class. This is great if you want to avoid having to write the same
piece of code several times or have to initialise an object that takes a lot of
computation time and, thereby, reducing your total test computation time.
Setting up a .travis.yml
¶
After having at least one file that contains unit tests, we can start setting
up the continuous integration. And as you might suspect, it’s also not as
hard as it sounds. You simply need to open an account on
travis-ci.com preferably using your github account (
or linking the both works, too I presume) and then add the
.travis.yml
to your repository. After adding the .travis.yml
,
travis-ci.com will automatically detect the file
and start running a test. This will start after every commit pull request etc.
So now, let’s look at one of these .travis.yml
files
(the one from matpy
)
The first few things are just system settings. The install
keyword
however introduces a sequence of bash commands that are executed.
Let’s go through this thing line by line.
Gets the anaconda installation file from the server.
Starts installation
Add miniconda path to PATH
hash -r (idk)
In installation always say yes and turn off command line environment indicator
Add package channels
Update conda
conda info -a
Create environment from environment file
Activate environment
Install package
The script
is the test script to be executed. If everything is executed
without errors, the continuous integration test will show up as passed.
That means you’re all set for continuous integration! Wasn’t all that hard was it?