Software Carpentry logo

Unit Testing

April 24, 2010: We are pleased to announce that Version 4 of this course is now under development. For updates and an early peek at the content, please check out the Software Carpentry blog at http://www.software-carpentry.org/blog/.

1) Introduction

2) JUnit and Its Children

3) You Can Skip This Lecture If...

4) The Big Idea

5) Checking

6) Example: Checking Addition

import unittest

class TestAddition(unittest.TestCase):

    def test_zeroes(self):
        self.assertEqual(0 + 0, 0)
        self.assertEqual(5 + 0, 5)
        self.assertEqual(0 + 13.2, 13.2)

    def test_positive(self):
        self.assertEqual(123 + 456, 579)
        self.assertEqual(1.2e20 + 3.4e20, 3.5e20)

    def test_mixed(self):
        self.assertEqual(-19 + 20, 1)
        self.assertEqual(999 + -1, 998)
        self.assertEqual(-300.1 + -400.2, -700.3)

if __name__ == '__main__':
    unittest.main()
.F.
======================================================================
FAIL: test_positive (__main__.TestAddition)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_addition.py", line 12, in test_positive
    self.assertEqual(1.2e20 + 3.4e20, 3.5e20)
AssertionError: 4.6e+20 != 3.5e+20

----------------------------------------------------------------------
Ran 3 tests in 0.000s

FAILED (failures=1)

7) Running Sums

8) Flawed Implementation

def running_sum(seq):
    result = seq[0:1]
    for i in range(2, len(seq)):
        result.append(result[i-1] + seq[i])
    return result

class SumTests(unittest.TestCase):

    def test_empty(self):
        self.assertEqual(running_sum([]), [])

    def test_single(self):
        self.assertEqual(running_sum([3]), [3])

    def test_double(self):
        self.assertEqual(running_sum([2, 9]), [2, 11])

    def test_long(self):
        self.assertEqual(running_sum([-3, 0, 3, -2, 5]), [-3, -3, 0, -2, 3])
F.E.
======================================================================
ERROR: test_long (__main__.SumTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "running_sum_wrong.py", line 22, in test_long
    self.assertEqual(running_sum([-3, 0, 3, -2, 5]), [-3, -3, 0, -2, 3])
  File "running_sum_wrong.py", line 7, in running_sum
    result.append(result[i-1] + seq[i])
IndexError: list index out of range

======================================================================
FAIL: test_double (__main__.SumTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "running_sum_wrong.py", line 19, in test_double
    self.assertEqual(running_sum([2, 9]), [2, 11])
AssertionError: [2] != [2, 11]

----------------------------------------------------------------------
Ran 4 tests in 0.001s

FAILED (failures=1, errors=1)

9) Check and Re-check

def running_sum(seq):
    result = seq[0:1]
    for i in range(1, len(seq)):
        result.append(result[i-1] + seq[i])
    return result
....
----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK

10) Is This Cost-Effective?

11) Eliminating Redundancy

class TestThiamine(unittest.TestCase):

    def setUp(self):
        self.fixture = Molecule(C=12, H=20, O=1, N=4, S=1)

    def test_erase_nothing(self):
        nothing = Molecule()
        self.fixture.erase(nothing)
        self.assertEqual(self.fixture['C'], 12)
        self.assertEqual(self.fixture['H'], 20)
        self.assertEqual(self.fixture['O'], 1)
        self.assertEqual(self.fixture['N'], 4)
        self.assertEqual(self.fixture['S'], 1)

    def test_erase_single(self):
        self.fixture.erase(Molecule(H=1))
        self.assertEqual(self.fixture, Molecule(C=12, H=19, O=1, N=4, S=1))

    def test_erase_self(self):
        self.fixture.erase(self.fixture)
        self.assertEqual(self.fixture, Molecule())
.E.
======================================================================
ERROR: test_erase_self (__main__.TestThiamine)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "setup.py", line 49, in test_erase_self
    self.fixture.erase(self.fixture)
  File "setup.py", line 21, in erase
    for k in other.atoms:
RuntimeError: dictionary changed size during iteration

----------------------------------------------------------------------
Ran 3 tests in 0.000s

FAILED (errors=1)

12) Testing Exceptions

13) Manual Exception Testing Example

class TestInRange(unittest.TestCase):

    def test_no_values(self):
        try:
            in_range([], 0.0, 1.0)
        except ValueError:
            pass
        else:
            self.fail()

    def test_bad_range(self):
        try:
            in_range([0.0], 4.0, -2.0)
        except ValueError:
            pass
        else:
            self.fail()

14) Testing I/O

15) I/O Testing Example

class TestDiff(unittest.TestCase):

    def wrap_and_run(self, left, right, expected):
        left = StringIO(left)
        right = StringIO(right)
        actual = StringIO()
        diff(left, right, actual)
        self.assertEqual(actual.getvalue(), expected)

    def test_empty(self):
        self.wrap_and_run('', '', '')

    def test_lengthy_match(self):
        str = '''\
a
b
c
'''
        self.wrap_and_run(str, str, '')

    def test_single_line_mismatch(self):
        self.wrap_and_run('a\n', 'b\n', '1\n')

    def test_middle_mismatch(self):
        self.wrap_and_run('a\nb\nc\n', 'a\nx\nc\n', '2\n')

16) Stubs and Mock Objects

17) Test Performance

18) Choosing Test Cases

19) Example: Rectangle Overlap

20) Solution

21) What Tests To Write First

22) Summary