Chapter 7. Test-driven Development Walk-through

In this chapter, we're not going to talk about new techniques of testing in Python, and we're not going to spend much time talking about the philosophy of testing. Instead, what we're going to do is a step-by-step walk-through of an actual development process. Your humble and sadly fallible author has commemorated his mistakes—and the ways that testing helped him fix them—while developing part of a personal scheduling program.

In this chapter, we'll cover the following topics:

  • Writing a testable specification
  • Writing unit tests that drive the development process
  • Writing code that complies with the specification and unit tests
  • Using the testable specification and unit tests to help debug

You'll be prompted to design and build your own module as you read through this chapter, so that you can walk through your own process as well.

Writing the specification

As usual, the process starts with a written specification. The specification is a doctest that we learned in Chapter 2, Working with doctest, and Chapter 3, Unit Testing with doctest, so the computer can use it to check the implementation. The specification isn't strictly a set of unit tests, though the discipline of unit testing has been sacrificed (for the moment) in exchange for making the document more accessible to a human reader. That's a common trade-off, and it's fine as long as you make up for it by also writing unit tests covering the code.

The goal of the project in this chapter is to make a Python package capable of representing personal time management information.

The following code goes in a file called docs/outline.txt:

This project is a personal scheduling system intended to keep track of
a single person's schedule and activities. The system will store and
display two kinds of schedule information: activities and statuses.
Activities and statuses both support a protocol which allows them to
be checked for overlap with another object supporting the protocol.

>>> from planner.data import Activity, Status
>>> from datetime import datetime

Activities and statuses are stored in schedules, to which they can be
added and removed.

>>> from planner.data import Schedule
>>> activity = Activity('test activity',
..                      datetime(year = 2014, month = 6, day = 1,
..                               hour = 10, minute = 15),
..                      datetime(year = 2014, month = 6, day = 1,
..                               hour = 12, minute = 30))
>>> duplicate_activity = Activity('test activity',
..                      datetime(year = 2014, month = 6, day = 1,
..                               hour = 10, minute = 15),
..                      datetime(year = 2014, month = 6, day = 1,
..                               hour = 12, minute = 30))
>>> status = Status('test status',
...                 datetime(year = 2014, month = 7, day = 1,
...                          hour = 10, minute = 15),
...                 datetime(year = 2014, month = 7, day = 1,
...                          hour = 12, minute = 30))
>>> schedule = Schedule()
>>> schedule.add(activity)
>>> schedule.add(status)
>>> status in schedule
True
>>> activity in schedule
True
>>> duplicate_activity in schedule
True
>>> schedule.remove(activity)
>>> schedule.remove(status)
>>> status in schedule
False
>>> activity in schedule
False

Activities represent tasks that the person must actively engage in,
and they are therefore mutually exclusive: no person can have two
activities that overlap the same period of time.

>>> activity1 = Activity('test activity 1',
...                      datetime(year = 2014, month = 6, day = 1,
...                               hour = 9, minute = 5),
...                      datetime(year = 2014, month = 6, day = 1,
...                               hour = 12, minute = 30))
>>> activity2 = Activity('test activity 2',
...                      datetime(year = 2014, month = 6, day = 1,
...                               hour = 10, minute = 15),
...                      datetime(year = 2014, month = 6, day = 1,
...                               hour = 13, minute = 30))
>>> schedule = Schedule()
>>> schedule.add(activity1)
>>> schedule.add(activity2)
Traceback (most recent call last):
ScheduleError: "test activity 2" overlaps with "test activity 1"

Statuses represent tasks that a person engages in passively, and so
can overlap with each other and with activities.

>>> activity1 = Activity('test activity 1',
...                      datetime(year = 2014, month = 6, day = 1,
...                               hour = 9, minute = 5),
...                      datetime(year = 2014, month = 6, day = 1,
...                               hour = 12, minute = 30))
>>> status1 = Status('test status 1',
...                  datetime(year = 2014, month = 6, day = 1,
...                           hour = 10, minute = 15),
...                  datetime(year = 2014, month = 6, day = 1,
...                           hour = 13, minute = 30))
>>> status2 = Status('test status 2',
...                  datetime(year = 2014, month = 6, day = 1,
...                           hour = 8, minute = 45),
...                  datetime(year = 2014, month = 6, day = 1,
...                           hour = 15, minute = 30))
>>> schedule = Schedule()
>>> schedule.add(activity1)
>>> schedule.add(status1)
>>> schedule.add(status2)
>>> activity1 in schedule
True
>>> status1 in schedule
True
>>> status2 in schedule
True

Schedules can be saved to a sqlite database, and they can be reloaded
from that stored state.

>>> from planner.persistence import file
>>> storage = File(':memory:')
>>> schedule.store(storage)
>>> newsched = Schedule.load(storage)
>>> schedule == newsched
True

This doctest will serve as a testable specification for my project, which means that it will be the foundation stone for all of my tests and my program code that will be built on. Let's look at each section in more detail:

This project is a personal scheduling system intended to keep track of
a single person's schedule and activities. The system will store and
display two kinds of schedule information: activities and statuses.
Activities and statuses both support a protocol which allows them to
be checked for overlap with another object supporting the protocol.

>>> from planner.data import Activity, Status
>>> from datetime import datetime

The preceding code consists of some introductory English text, and a couple of import statements that bring in code that we need for these tests. By doing so, they also tell us about some of the structure of the planner package. It contains a module called data that defines Activity and Status.

Activities and statuses are stored in schedules, to which they can be
added and removed.

>>> from planner.data import Schedule
>>> activity = Activity('test activity',
..                      datetime(year = 2014, month = 6, day = 1,
..                               hour = 10, minute = 15),
..                      datetime(year = 2014, month = 6, day = 1,
..                               hour = 12, minute = 30))
>>> duplicate_activity = Activity('test activity',
..                      datetime(year = 2014, month = 6, day = 1,
..                               hour = 10, minute = 15),
..                      datetime(year = 2014, month = 6, day = 1,
..                               hour = 12, minute = 30))
>>> status = Status('test status',
...                 datetime(year = 2014, month = 7, day = 1,
...                          hour = 10, minute = 15),
...                 datetime(year = 2014, month = 7, day = 1,
...                          hour = 12, minute = 30))
>>> schedule = Schedule()
>>> schedule.add(activity)
>>> schedule.add(status)
>>> status in schedule
True
>>> activity in schedule
True
>>> duplicate_activity in schedule
True
>>> schedule.remove(activity)
>>> schedule.remove(status)
>>> status in schedule
False
>>> activity in schedule
False

The preceding tests describe some of the desired behavior of the Schedule instances when interacting with the Activity and Status objects. According to these tests, a Schedule instance must accept an Activity or Status object as the parameter of its add and remove methods; once added, the in operator must return True for an object until it is removed. Furthermore, the two Activity instances that have the same parameters must be treated as the same object by Schedule:

Activities represent tasks that the person must actively engage in,
and they are therefore mutually exclusive: no person can have two
activities that overlap the same period of time.

>>> activity1 = Activity('test activity 1',
...                      datetime(year = 2014, month = 6, day = 1,
...                               hour = 9, minute = 5),
...                      datetime(year = 2014, month = 6, day = 1,
...                               hour = 12, minute = 30))
>>> activity2 = Activity('test activity 2',
...                      datetime(year = 2014, month = 6, day = 1,
...                               hour = 10, minute = 15),
...                      datetime(year = 2014, month = 6, day = 1,
...                               hour = 13, minute = 30))
>>> schedule = Schedule()
>>> schedule.add(activity1)
>>> schedule.add(activity2)
Traceback (most recent call last):
ScheduleError: "test activity 2" overlaps with "test activity 1"

The preceding test code describes what should happen when overlapping activities are added to a schedule. Specifically, a ScheduleError exception should be raised:

Statuses represent tasks that a person engages in passively, and so
can overlap with each other and with activities.

>>> activity1 = Activity('test activity 1',
...                      datetime(year = 2014, month = 6, day = 1,
...                               hour = 9, minute = 5),
...                      datetime(year = 2014, month = 6, day = 1,
...                               hour = 12, minute = 30))
>>> status1 = Status('test status 1',
...                  datetime(year = 2014, month = 6, day = 1,
...                           hour = 10, minute = 15),
...                  datetime(year = 2014, month = 6, day = 1,
...                           hour = 13, minute = 30))
>>> status2 = Status('test status 2',
...                  datetime(year = 2014, month = 6, day = 1,
...                           hour = 8, minute = 45),
...                  datetime(year = 2014, month = 6, day = 1,
...                           hour = 15, minute = 30))
>>> schedule = Schedule()
>>> schedule.add(activity1)
>>> schedule.add(status1)
>>> schedule.add(status2)
>>> activity1 in schedule
True
>>> status1 in schedule
True
>>> status2 in schedule
True

The preceding test code describes what should happen when overlapping statuses are added to a schedule: the schedule should accept them. Furthermore, if a status and an activity overlap, they can still both be added:

Schedules can be saved to a sqlite database, and they can be reloaded
from that stored state.

>>> from planner.persistence import file
>>> storage = File(':memory:')
>>> schedule.store(storage)
>>> newsched = Schedule.load(storage)
>>> schedule == newsched
True

The preceding code describes how schedule storage should work. It also tells us that the planner package needs to contain a persistence module that, in turn, should contain File. It also tells us that Schedule instances should have load and store methods, and that the == operator should return True when they contain the same data.

Try it for yourself – what are you going to do?

It's time for you to come up with a project of your own, something you can work on for yourself. We step through the development process:

  1. Think of a project of approximately the same complexity as the one described in this chapter. It should be a single module or a few modules in a single package. It should also be something that interests you, which is why I haven't given you a specific assignment here.

    Imagine that the project is already done, and you need to write a description of what you've done, along with a little bit of demonstration code. Then go ahead and write your description and demo code in the form of a doctest file.

  2. As you're writing the doctest file, watch out for places where your original idea has to change a little bit to make the demo easier to write or work better. When you find such cases, pay attention to them! At this stage, it's better to change the idea a little bit and save yourself effort all through the process.

Wrapping up the specification

We've now got testable specifications for a couple of moderately-sized projects—yours and mine. These will help us to write unit tests and code, and they'll give us a sense of how complete each project is as a whole.

In addition, the process of writing code into the doctest gave us a chance to test-drive our ideas. We've probably improved on our projects a little bit by using them in a concrete manner, even though the project implementation is still merely imaginary.

Once again, it's important that we have these tests written before writing the code that they will test. By writing the tests first, we give ourselves a touchstone that we can use in order to judge how well our code conforms to what we intended. If we write the code first, and then the tests, all we end up doing is enshrining what the code actually does—as opposed to what we meant for it to do—into the tests.

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

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