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.

  1. Gets the anaconda installation file from the server.

  2. Starts installation

  3. Add miniconda path to PATH

  4. hash -r (idk)

  5. In installation always say yes and turn off command line environment indicator

  6. Add package channels

  7. Update conda

  8. conda info -a

  9. Create environment from environment file

  10. Activate environment

  11. 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?