Chapter 41
Writing Basic Unit Tests

Difficulty

Newcomer

Skills

Problem/Task

As you know by now, Zope 3 gains its incredible stability from testing any code in great detail. The currently most common method is to write unit tests. This chapter introduces unit tests - which are Zope 3 independent - and introduces some of the subtleties.

Solution

41.1 Implementing the Sample Class

Before we can write tests, we have to write some code that we can test. Here, we will implement a simple class called Sample with a public attribute title and description that is accessed via getDescription() and mutated using setDescription(). Further, the description must be either a regular or unicode string.

Since this code will not depend on Zope, open a file named test_sample.py anywhere and add the following class:


1  Sample(object):
2      """A trivial Sample object."""
3  
4      title = None
5  
6      def __init__(self):
7          """Initialize object."""
8          self._description = ''
9  
10      def setDescription(self, value):
11          """Change the value of the description."""
12          assert isinstance(value, (str, unicode))
13          self._description = value
14  
15      def getDescription(self):
16          """Change the value of the description."""
17          return self._description

If you wish you can now manually test the class with the interactive Python shell. Just start Python by entering python in your shell prompt. Note that you should be in the directory in which test_sample.py is located when starting Python (an alternative is of course to specify the directory in your PYTHONPATH.)


1  >>> from test_sample import Sample
2  >>> sample = Sample()
3  >>> print sample.title
4  None
5  >>> sample.title = 'Title'
6  >>> print sample.title
7  Title
8  >>> print sample.getDescription()
9  
10  >>> sample.setDescription('Hello World')
11  >>> print sample.getDescription()
12  Hello World
13  >>> sample.setDescription(None)
14  Traceback (most recent call last):
15    File "<stdin>", line 1, in ?
16    File "test_sample.py", line 31, in setDescription
17      assert isinstance(value, (str, unicode))
18  AssertionError

As you can see in the last test, non-string object types are not allowed as descriptions and an AssertionError is raised.

41.2 Writing the Unit Tests

The goal of writing the unit tests is to convert this informal, manual, and interactive testing session into a formal test class. Python provides already a module called unittest for this purpose, which is a port of the Java-based unit testing product, JUnit, by Kent Beck and Erich Gamma. There are three levels to the testing framework (this list deviates a bit from the original definitions as found in the Python library documentation. 1 ).

The smallest unit is obviously the “test”, which is a single method in a TestCase class that tests the behavior of a small piece of code or a particular aspect of an implementation. The “test case” is then a collection tests that share the same setup/inputs. On top of all of this sits the “test suite” which is a collection of test cases and/or other test suites. Test suites combine tests that should be executed together. With the correct setup (as shown in the example below), you can then execute test suites. For large projects like Zope 3, it is useful to know that there is also the concept of a test runner, which manages the test run of all or a set of tests. The runner provides useful feedback to the application, so that various user interaces can be developed on top of it.

But enough about the theory. In the following example, which you can simply put into the same file as your code above, you will see a test in common Zope 3 style.


1  import unittest
2  
3  class SampleTest(unittest.TestCase):
4      """Test the Sample class"""
5  
6      def test_title(self):
7          sample = Sample()
8          self.assertEqual(sample.title, None)
9          sample.title = 'Sample Title'
10          self.assertEqual(sample.title, 'Sample Title')
11  
12      def test_getDescription(self):
13          sample = Sample()
14          self.assertEqual(sample.getDescription(), '')
15          sample._description = "Description"
16          self.assertEqual(sample.getDescription(), 'Description')
17  
18      def test_setDescription(self):
19          sample = Sample()
20          self.assertEqual(sample._description, '')
21          sample.setDescription('Description')
22          self.assertEqual(sample._description, 'Description')
23          sample.setDescription(u'Description2')
24          self.assertEqual(sample._description, u'Description2')
25          self.assertRaises(AssertionError, sample.setDescription, None)
26  
27  
28  def test_suite():
29      return unittest.TestSuite((
30          unittest.makeSuite(SampleTest),
31          ))
32  
33  if __name__ == '__main__':
34      unittest.main(defaultTest='test_suite')

41.3 Running the Tests

You can run the test by simply calling pythontest_sample.py from the directory you saved the file in. Here is the result you should see:


1  ...
2  ----------------------------------------------------------------------
3  Ran 3 tests in 0.001s
4  
5  OK

The three dots represent the three tests that were run. If a test had failed, it would have been reported pointing out the failing test and providing a small traceback.

When using the default Zope 3 test runner, tests will be picked up as long as they follow some conventions.

In our case, you could simply create a tests package in ZOPE3/src (do not forget the __init__.py file). Then place the test_sample.py file into this directory.

You you can use the test runner to run only the sample tests as follows from the Zope 3 root directory:


1  python test.py -vp tests.test_sample

The -v option stands for verbose mode, so that detailed information about a test failure is provided. The -p option enables a progress bar that tells you how many tests out of all have been completed. There are many more options that can be specified. You can get a full list of them with the option -h: pythontest.py-h.

The output of the call above is as follows:


1  Configuration file found.
2  Running UNIT tests at level 1
3  Running UNIT tests from /opt/zope/Zope3
4     3/3 (100.0%): test_title (tests.test_sample.SampleTest)
5  ----------------------------------------------------------------------
6  Ran 3 tests in 0.002s
7  
8  OK
9  Running FUNCTIONAL tests at level 1
10  Running FUNCTIONAL tests from /opt/zope/Zope3
11  
12  ----------------------------------------------------------------------
13  Ran 0 tests in 0.000s
14  
15  OK

Exercises

  1. It is not very common to do the setup - in our case sample=Sample() - in every test method. Instead there exists a method called setUp() and its counterpart tearDown that are run before and after each test, respectively. Change the test code above, so that it uses the setUp() method. In later chapters and the rest of the book we will frequently use this method of setting up tests.
  2. Currently the test_setDescription() test only verifies that None is not allowed as input value.

    1. Improve the test, so that all other builtin types are tested as well.
    2. Also, make sure that any objects inheriting from str or unicode pass as valid values.