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.
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:
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.
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.
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:
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.
3.147.84.157