Unit-tests

Unit-tests are not only a Python-specific concept, but a general paradigm to automate testing your code. When writing a program (or a milestone for a larger program) you usually have a certain goal in mind, and you will want to confirm that your code actually reaches this goal. To do so you usually test your code manually at first.


In [2]:
def sum(a, b):
    return a+b

#You test this before using it in a large project:
print 17+4
print sum(17, 4)


21
21

By comparing the result with the expected result, you can quickly check, whether your method is correct. However you cannot be sure, whether this works for every input in the sense that you didn't test it. So in practice one verifies code for several exemplary inputs and tries to catch all edgy cases. Keep that in mind, but note that this section is about automating testing; proper test-design is a topic for itself.

If the code is complex, much more output might be needed to check correctness. So it pays off to automate the process of checking this. For instance you could do the following:


In [3]:
def sum(a, b):
    return a+b

print 17+4 == sum(17, 4)


True

Now you don't have to compare the output yourself, but instead the script just tells you whether it worked.

Programmers tend to check code only after writing it. When they have confirmed that it works, they easily forget about it. But in complex projects, subtle changes can silently break code that used to work. So it makes sense to repeat such tests from time to time, e.g. after big changes or version-updates of dependencies.

The unit-test framework provides a standard way to organize your tests such that they won't get lost. It also provides tools to test Python version-specific aspects or platform-specific aspects.

import unittest

def sum(a, b):
    return a+b


class SumTests(unittest.TestCase):
    def test_sum(self):
        self.assertEqual(17+4, sum(17, 4))

    def test_raises(self):
        self.assertRaises(TypeError, sum, (6, None))

if __name__ == "__main__":
    unittest.main()

Cheat-sheet:

  • assertEqual(a, b) a == b
  • assertNotEqual(a, b) a != b
  • assertTrue(x) bool(x) is True
  • assertFalse(x) bool(x) is False
  • assertIs(a, b) a is b
  • assertIsNot(a, b) a is not b
  • assertIsNone(x) x is None
  • assertIsNotNone(x) x is not None
  • assertIn(a, b) a in b
  • assertNotIn(a, b) a not in b
  • assertIsInstance(a, b) isinstance(a, b)
  • assertNotIsInstance(a, b) not isinstance(a, b)
  • assertRaises(exc, fun, *args, **kwds) fun(*args, **kwds) raises exc
  • assertRaisesRegexp(exc, r, fun, *args, **kwds) fun(*args, **kwds) raises exc and the message matches regex r
  • assertAlmostEqual(a, b) round(a-b, 7) == 0
  • assertNotAlmostEqual(a, b) round(a-b, 7) != 0
  • assertGreater(a, b) a > b
  • assertGreaterEqual(a, b) a >= b
  • assertLess(a, b) a < b
  • assertLessEqual(a, b) a <= b
  • assertRegexpMatches(s, r) r.search(s)
  • assertNotRegexpMatches(s, r) not r.search(s)
  • assertItemsEqual(a, b) sorted(a) == sorted(b) and works with unhashable objs
  • assertDictContainsSubset(a, b) all the key/value pairs in a exist in b

For a complete documentation, see https://docs.python.org/2/library/unittest.html

Managing test attributes or resources

Sometimes it is necessary to load specific resources for a bunch of tests or to set certain flags and attributes that test some code under hard circumstances.

import unittest

def sum(a, b):
    return a+b


class SumTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        print 'set up class'

    @classmethod
    def tearDownClass(cls):
        print 'tear down class'

    def setUp(self):
        print 'set up test...'

    def tearDown(self):
        print 'tear down test...'

    def test_sum(self):
        self.assertEqual(17+4, sum(17, 4))

    def test_raises(self):
        self.assertRaises(TypeError, sum, (6, None))

if __name__ == "__main__":
    unittest.main()

Skipping tests

To skip tests that are under construction, or are platform/version specific there are a number of decorators. Also, if - e.g. due to refactoring - some tests temporarily fail, you can mark them as expected failures.

import unittest

class MyTestCase(unittest.TestCase):

    @unittest.skip("demonstrating skipping")
    def test_nothing(self):
        self.fail("shouldn't happen")

    @unittest.skipIf(mylib.__version__ < (1, 3),
                     "not supported in this library version")
    def test_format(self):
        # Tests that work for only a certain version of the library.
        pass

    @unittest.skipUnless(sys.platform.startswith("win"), "requires Windows")
    def test_windows_support(self):
        # windows specific testing code
        pass

    @unittest.expectedFailure
    def test_fail(self):
        self.assertEqual(1, 0, "broken")

You can also skip an entire class:

@unittest.skip("showing class skipping")
class MySkippedTestCase(unittest.TestCase):
    def test_not_run(self):
        pass

In [ ]: