Writing initial unit tests

Since the specification doesn't contain unit tests, there's still a need for unit tests before the coding of the module can begin. The planner.data classes are the first target for the implementation, so they're the first ones to get the tests.

Activities and statuses are defined to be very similar, so their test modules are also similar. They're not identical, though, and they're not required to have any particular inheritance relationship; so the tests remain distinct.

The following tests are in tests/test_activities.py:

from unittest import TestCase
from unittest.mock import patch, Mock
from planner.data import Activity, TaskError
from datetime import datetime

class constructor_tests(TestCase):
    def test_valid(self):
        activity = Activity('activity name',
                           datetime(year = 2012, month = 9, day = 11),
                           datetime(year = 2013, month = 4, day = 27))

        self.assertEqual(activity.name, 'activity name')
        self.assertEqual(activity.begins,
                         datetime(year = 2012, month = 9, day = 11))
        self.assertEqual(activity.ends,
                         datetime(year = 2013, month = 4, day = 27))

    def test_backwards_times(self):
        self.assertRaises(TaskError,
                          Activity,
                          'activity name',
                          datetime(year = 2013, month = 4, day = 27),
                          datetime(year = 2012, month = 9, day = 11))

    def test_too_short(self):
        self.assertRaises(TaskError,
                          Activity,
                          'activity name',
                          datetime(year = 2013, month = 4, day = 27,
                                   hour = 7, minute = 15),
                          datetime(year = 2013, month = 4, day = 27,
                                   hour = 7, minute = 15))

class utility_tests(TestCase):
    def test_repr(self):
        activity = Activity('activity name',
                           datetime(year = 2012, month = 9, day = 11),
                           datetime(year = 2013, month = 4, day = 27))

        expected = "<activity name 2012-09-11T00:00:00 2013-04-27T00:00:00>"

        self.assertEqual(repr(activity), expected)

class exclusivity_tests(TestCase):
    def test_excludes(self):
        activity = Mock()

        other = Activity('activity name',
                         datetime(year = 2012, month = 9, day = 11),
                         datetime(year = 2012, month = 10, day = 6))

        # Any activity should exclude any activity
        self.assertTrue(Activity.excludes(activity, other))

        # Anything not known to be excluded should be included
        self.assertFalse(Activity.excludes(activity, None))

class overlap_tests(TestCase):
    def test_overlap_before(self):
        activity = Mock(begins = datetime(year = 2012, month = 9, day = 11),
                        ends = datetime(year = 2012, month = 10, day = 6))

        other = Mock(begins = datetime(year = 2012, month = 10, day = 7),
                     ends = datetime(year = 2013, month = 2, day = 5))

        self.assertFalse(Activity.overlaps(activity, other))

    def test_overlap_begin(self):
        activity = Mock(begins = datetime(year = 2012, month = 8, day = 11),
                        ends = datetime(year = 2012, month = 11, day = 27))

        other = Mock(begins = datetime(year = 2012, month = 10, day = 7),
                     ends = datetime(year = 2013, month = 2, day = 5))

        self.assertTrue(Activity.overlaps(activity, other))

    def test_overlap_end(self):
        activity = Mock(begins = datetime(year = 2013, month = 1, day = 11),
                        ends = datetime(year = 2013, month = 4, day = 16))

        other = Mock(begins = datetime(year = 2012, month = 10, day = 7),
                     ends = datetime(year = 2013, month = 2, day = 5))

        self.assertTrue(Activity.overlaps(activity, other))

    def test_overlap_inner(self):
        activity = Mock(begins = datetime(year = 2012, month = 10, day = 11),
                        ends = datetime(year = 2013, month = 1, day = 27))

        other = Mock(begins = datetime(year = 2012, month = 10, day = 7),
                     ends = datetime(year = 2013, month = 2, day = 5))

        self.assertTrue(Activity.overlaps(activity, other))

    def test_overlap_outer(self):
        activity = Mock(begins = datetime(year = 2012, month = 8, day = 12),
                        ends = datetime(year = 2013, month = 3, day = 15))

        other = Mock(begins = datetime(year = 2012, month = 10, day = 7),
                     ends = datetime(year = 2013, month = 2, day = 5))

        self.assertTrue(Activity.overlaps(activity, other))

    def test_overlap_after(self):
        activity = Mock(begins = datetime(year = 2013, month = 2, day = 6),
                        ends = datetime(year = 2013, month = 4, day = 27))

        other = Mock(begins = datetime(year = 2012, month = 10, day = 7),
                     ends = datetime(year = 2013, month = 2, day = 5))

        self.assertFalse(Activity.overlaps(activity, other))

Let's take a look at the following code, step-by-step:

    def test_valid(self):
        activity = Activity('activity name',
                            datetime(year = 2012, month = 9, day = 11),
                            datetime(year = 2013, month = 4, day = 27))

        self.assertEqual(activity.name, 'activity name')
        self.assertEqual(activity.begins,
                         datetime(year = 2012, month = 9, day = 11))
        self.assertEqual(activity.ends,
                         datetime(year = 2013, month = 4, day = 27))

The test_valid method checks whether the constructor works correctly when all of the parameters are correct. This is an important test, because it defines what correct behavior should be normally. We need more tests, though, to define correct behavior in abnormal situations:

    def test_backwards_times(self):
        self.assertRaises(TaskError,
                          Activity,
                          'activity name',
                          datetime(year = 2013, month = 4, day = 27),
                          datetime(year = 2012, month = 9, day = 11))

Here, we're making sure that you can't create an activity that ends before it begins. That doesn't make any sense, and can easily throw off assumptions made during the implementation:

    def test_too_short(self):
        self.assertRaises(TaskError,
                          Activity,
                          'activity name',
                          datetime(year = 2013, month = 4, day = 27,
                                   hour = 7, minute = 15),
                          datetime(year = 2013, month = 4, day = 27,
                                   hour = 7, minute = 15))

We don't want extremely short activities, either. In the real world, an activity that takes no time is meaningless, so we have a test here to make sure that such things are not allowed:

class utility_tests(TestCase):
    def test_repr(self):
        activity = Activity('activity name',
                            datetime(year = 2012, month = 9, day = 11),
                            datetime(year = 2013, month = 4, day = 27))

        expected = "<activity name 2012-09-11T00:00:00 2013-04-27T00:00:00>"

        self.assertEqual(repr(activity), expected)

While repr(activity) isn't likely to be used in any production code paths, it's handy during development and debugging. This test defines how the text representation of an activity ought to look, to make sure that it contains the desired information.

Tip

The repr function is often useful during debugging, because it attempts to take any object and turn it into a string that represents that object. This is distinct from the str function, because str tries to turn the object into a string that is convenient for humans to read. The repr function, on the other hand, tries to create a string containing code that will recreate the object. That's a slightly tough concept, so here's an example contrasting str and repr:

>>> from decimal import Decimal
>>> x = Decimal('123.45678')
>>> str(x)
'123.45678'
>>> repr(x)
"Decimal('123.45678')"
class exclusivity_tests(TestCase):
    def test_excludes(self):
        activity = Mock()

        other = Activity('activity name',
                         datetime(year = 2012, month = 9, day = 11),
                         datetime(year = 2012, month = 10, day = 6))

        # Any activity should exclude any activity
        self.assertTrue(Activity.excludes(activity, other))

        # Anything not known to be excluded should be included
        self.assertFalse(Activity.excludes(activity, None))

It's up to the objects stored in a schedule to decide whether they are exclusive with other objects they overlap. Specifically, activities are supposed to exclude each other, so we check this here. We're using a mock object for the main activity, but we're being a bit lazy and use a real Activity instance to compare it against, trusting that there won't be a problem in this case. We don't expect that Activity.excludes will do much more than apply the isinstance function to its parameter, so there's not much that an error in the constructor can do to mess things up.

class overlap_tests(TestCase):
    def test_overlap_before(self):
        activity = Mock(begins = datetime(year = 2012, month = 9, day = 11),
                        ends = datetime(year = 2012, month = 10, day = 6))

        other = Mock(begins = datetime(year = 2012, month = 10, day = 7),
                     ends = datetime(year = 2013, month = 2, day = 5))

        self.assertFalse(Activity.overlaps(activity, other))

    def test_overlap_begin(self):
        activity = Mock(begins = datetime(year = 2012, month = 8, day = 11),
                        ends = datetime(year = 2012, month = 11, day = 27))

        other = Mock(begins = datetime(year = 2012, month = 10, day = 7),
                     ends = datetime(year = 2013, month = 2, day = 5))

        self.assertTrue(Activity.overlaps(activity, other))

    def test_overlap_end(self):
        activity = Mock(begins = datetime(year = 2013, month = 1, day = 11),
                        ends = datetime(year = 2013, month = 4, day = 16))

        other = Mock(begins = datetime(year = 2012, month = 10, day = 7),
                     ends = datetime(year = 2013, month = 2, day = 5))

        self.assertTrue(Activity.overlaps(activity, other))

    def test_overlap_inner(self):
        activity = Mock(begins = datetime(year = 2012, month = 10, day = 11),
                        ends = datetime(year = 2013, month = 1, day = 27))

        other = Mock(begins = datetime(year = 2012, month = 10, day = 7),
                     ends = datetime(year = 2013, month = 2, day = 5))

        self.assertTrue(Activity.overlaps(activity, other))

    def test_overlap_outer(self):
        activity = Mock(begins = datetime(year = 2012, month = 8, day = 12),
                        ends = datetime(year = 2013, month = 3, day = 15))

        other = Mock(begins = datetime(year = 2012, month = 10, day = 7),
                     ends = datetime(year = 2013, month = 2, day = 5))

        self.assertTrue(Activity.overlaps(activity, other))

    def test_overlap_after(self):
        activity = Mock(begins = datetime(year = 2013, month = 2, day = 6),
                        ends = datetime(year = 2013, month = 4, day = 27))

        other = Mock(begins = datetime(year = 2012, month = 10, day = 7),
                     ends = datetime(year = 2013, month = 2, day = 5))

        self.assertFalse(Activity.overlaps(activity, other))

These tests describe the behavior of the code that checks whether activities overlap in the cases where the first activity:

  • Comes before the second activity
  • Overlaps the beginning of the second activity
  • Overlaps the end of the second activity
  • Begins and ends within the range of the second activity
  • Begins before the second activity and ends after it
  • Comes after the second activity

This covers the domain of possible relationships between the tasks.

No actual activities were used in these tests, just Mock objects that had been given the attributes that the Activity.overlaps function should look for. As always, we're doing our best to keep the code in different units from being able to interact during the tests.

Tip

You might have noticed that we used a shortcut to create the mock objects, by passing the attributes, we wanted them to have as keyword parameters for the constructor. Most of the time, that's a handy way to save a little work, but it does have the problem that it only works for attribute names that don't happen to be used as actual parameters to the Mock constructor. Notably, attributes called name can't be assigned in this way, because that parameter has a special meaning for Mock.

The code in tests/test_statuses.py is almost the same, except that it uses the Status class instead of the Activity class. There is one significant difference, though:

    def test_excludes(self):
        status = Mock()

        other = Status('status name',
                       datetime(year = 2012, month = 9, day = 11),
                       datetime(year = 2012, month = 10, day = 6))

        # A status shouldn't exclude anything
        self.assertFalse(Status.excludes(status, other))
        self.assertFalse(Status.excludes(status, None))

The defining difference between a Status and an Activity is that a status does not exclude other tasks that overlap with it. The tests, naturally, should reflect that difference.

The following code goes in tests/test_schedules.py. We define several mock objects that behave as if they were statuses or activities, and in which they support the overlap and exclusion protocol. We'll use these mock objects in several tests, to see how the schedule deals with the various combinations of overlapping and exclusive objects:

from unittest import TestCase
from unittest.mock import patch, Mock
from planner.data import Schedule, ScheduleError
from datetime import datetime

class add_tests(TestCase):
    overlap_exclude = Mock()
    overlap_exclude.overlaps = Mock(return_value = True)
    overlap_exclude.excludes = Mock(return_value = True)

    overlap_include = Mock()
    overlap_include.overlaps = Mock(return_value = True)
    overlap_include.excludes = Mock(return_value = False)

    distinct_exclude = Mock()
    distinct_exclude.overlaps = Mock(return_value = False)
    distinct_exclude.excludes = Mock(return_value = True)

    distinct_include = Mock()
    distinct_include.overlaps = Mock(return_value = False)
    distinct_include.excludes = Mock(return_value = False)

    def test_add_overlap_exclude(self):
        schedule = Schedule()
        schedule.add(self.distinct_include)
        self.assertRaises(ScheduleError,
                          schedule.add,
                          self.overlap_exclude)

    def test_add_overlap_include(self):
        schedule = Schedule()
        schedule.add(self.distinct_include)
        schedule.add(self.overlap_include)

    def test_add_distinct_exclude(self):
        schedule = Schedule()
        schedule.add(self.distinct_include)
        schedule.add(self.distinct_exclude)

    def test_add_distinct_include(self):
        schedule = Schedule()
        schedule.add(self.distinct_include)
        schedule.add(self.distinct_include)

    def test_add_over_overlap_exclude(self):
        schedule = Schedule()
        schedule.add(self.overlap_exclude)
        self.assertRaises(ScheduleError,
                          schedule.add,
                          self.overlap_include)

    def test_add_over_distinct_exclude(self):
        schedule = Schedule()
        schedule.add(self.distinct_exclude)
        self.assertRaises(ScheduleError,
                          schedule.add,
                          self.overlap_include)

    def test_add_over_overlap_include(self):
        schedule = Schedule()
        schedule.add(self.overlap_include)
        schedule.add(self.overlap_include)

    def test_add_over_distinct_include(self):
        schedule = Schedule()
        schedule.add(self.distinct_include)
        schedule.add(self.overlap_include)

class in_tests(TestCase):
    fake = Mock()
    fake.overlaps = Mock(return_value = True)
    fake.excludes = Mock(return_value = True)

    def test_in_before_add(self):
        schedule = Schedule()
        self.assertFalse(self.fake in schedule)

    def test_in_after_add(self):
        schedule = Schedule()
        schedule.add(self.fake)
        self.assertTrue(self.fake in schedule)

Let's take a closer look at some sections of the following code:

    overlap_exclude = Mock()
    overlap_exclude.overlaps = Mock(return_value = True)
    overlap_exclude.excludes = Mock(return_value = True)

    overlap_include = Mock()
    overlap_include.overlaps = Mock(return_value = True)
    overlap_include.excludes = Mock(return_value = False)

    distinct_exclude = Mock()
    distinct_exclude.overlaps = Mock(return_value = False)
    distinct_exclude.excludes = Mock(return_value = True)

    distinct_include = Mock()
    distinct_include.overlaps = Mock(return_value = False)
    distinct_include.excludes = Mock(return_value = False)

These lines create mock objects as attributes of the add_tests class. Each of these mock objects has mocked overlaps and excludes methods that will always return either True or False when called. This means that each of these mock objects considers itself as overlap ping either everything or nothing, and excludes either everything or nothing. Between the four mock objects, we have covered all the possible combinations. In the following tests, we'll add various combinations of these mock objects to a schedule, and make sure that it does the right things:

    def test_add_overlap_exclude(self):
        schedule = Schedule()
        schedule.add(self.distinct_include)
        self.assertRaises(ScheduleError,
                          schedule.add,
                          self.overlap_exclude)

    def test_add_overlap_include(self):
        schedule = Schedule()
        schedule.add(self.distinct_include)
        schedule.add(self.overlap_include)

    def test_add_distinct_exclude(self):
        schedule = Schedule()
        schedule.add(self.distinct_include)
        schedule.add(self.distinct_exclude)

    def test_add_distinct_include(self):
        schedule = Schedule()
        schedule.add(self.distinct_include)
        schedule.add(self.distinct_include)

The preceding four tests are covering cases where we add a nonoverlapping object to a schedule. All of them are expected to accept the nonoverlapping object, except the first. In this test, we've previously added an object that claims that it does indeed overlap; furthermore, it excludes anything it overlaps. This test shows that, if either the object being added or an object already in the schedule believes that there's an overlap, the schedule must treat it as an overlap.

    def test_add_over_overlap_exclude(self):
        schedule = Schedule()
        schedule.add(self.overlap_exclude)
        self.assertRaises(ScheduleError,
                          schedule.add,
                          self.overlap_include)

In this test, we're making sure that if an object already in the schedule overlaps a new object and claims exclusivity, then adding the new object will fail.

    def test_add_over_distinct_exclude(self):
        schedule = Schedule()
        schedule.add(self.distinct_exclude)
        self.assertRaises(ScheduleError,
                          schedule.add,
                          self.overlap_include)

In this test, we're making sure that, even though the object already in the schedule doesn't think that it overlaps with the new object, it excludes the new object because the new object thinks that there's an overlap.

    def test_add_over_overlap_include(self):
        schedule = Schedule()
        schedule.add(self.overlap_include)
        schedule.add(self.overlap_include)

    def test_add_over_distinct_include(self):
        schedule = Schedule()
        schedule.add(self.distinct_include)
        schedule.add(self.overlap_include)

These tests are making sure that the inclusive objects don't somehow interfere with adding each other to a schedule.

class in_tests(TestCase):
    fake = Mock()
    fake.overlaps = Mock(return_value = True)
    fake.excludes = Mock(return_value = True)

    def test_in_before_add(self):
        schedule = Schedule()
        self.assertFalse(self.fake in schedule)

    def test_in_after_add(self):
        schedule = Schedule()
        schedule.add(self.fake)
        self.assertTrue(self.fake in schedule)

These two tests describe the schedule behavior with respect to the in operator. Specifically, it should return True when the object in question is actually in the schedule.

Try it for yourself – write your early unit tests

A specification—even a testable specification written in doctest—still hosts a lot of ambiguities that can be ironed out with good unit tests. Add that to the fact that the specification doesn't maintain separation between different tests, and you can see that it's time for your project to gain some unit tests. Perform the following steps:

  1. Find some element of your project that is described in (or implied by) your specification.
  2. Write a unit test that describes the behavior of that element when given the correct input.
  3. Write a unit test that describes the behavior of that element when given the incorrect input.
  4. Write unit tests that describe the behavior of the element at the boundaries between correct and incorrect input.
  5. Go back to step 1 if you can find another untested part of your program.

Wrapping up the initial unit tests

This is where you really take what was an ill-defined idea and turn it into a precise description of what you're going to do.

The end result can be quite lengthy, which shouldn't come as much of a surprise. After all, your goal at this stage is to completely define the behavior of your project; even without concerning yourself with the details of how that behavior is implemented, that's a lot of information.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.128.173.53