Initial Tests and Model Objects

Begin transcribing the application requirements into tests and perform the first coding cycle to satisfy the tests. This is the first exposure to working with model objects and unit testing.

Application Root

It’s time to start implementing the application requirements. If we review the entity outline we created, we can see that our top-level object is our application root which contains “Performance” objects.

If we look at the class that was generated by grokproject (in the project directory at ~/grok/virtualgrok/music/src/music/app.py), we see that it is called “Music”. This was the name we provided to grokproject with the first letter capitalized to match the naming convention for classes.

import grok
class Music(grok.Application, grok.Container):
    pass
class Index(grok.View):
    pass # see app_templates/index.pt

By looking at this class, we see that it inherits from two base classes. The first is grok.Application. This is what gets the class listed in the Grok Admin UI so that you can make instances of the application as we discussed in the previous tutorial page.

The other base class is grok.Container. This means that the class will be a dictionary-like structure that will contain key-value pairs where the values will be objects and they can be accessed using unique strings as the keys.

There are several things that this container does beyond a standard dictionary. First, it automatically persists objects placed in it to the Zope object database (ZODB). Secondly, it keeps an efficient index of its contents so that it can quickly find objects even when it contains a fairly large quantity. Lastly, it records the object hierarchy to assist code that interprets an URL in order to find a particular object in the database.

Requirements for Adding a Performance

We know from the requirements that we are going to store our performances by date. Since the key is a string, we could let the user enter any sort of date-like string for the performance and use that. The problem will be that, not only will this be hard to sort and read, but some of the strings a user might provide would require some really ugly encoding to work as part of an URL. (Remember, the URL’s are supposed to understandable and a date encoded like “03%2F15%2F2009” is hard to read.)

We also know that the minimum date and time resolution for performances is an hour. A text representation of a date as “yyyy-mm-dd” with an optional “-hh” suffix will provide the resolution we need as well as provide chronological ordering by simply iterating over the container’s keys.

Now, how do we enforce this rule? We could do it as part of an HTML form validation or we could try to encapsulate the container object so that performances can only be added through a method that evaluates the user input against the rule. If we do it in the form or the view code, it will have to be functionally tested as opposed to writing a unit test.

Encapsulating to force coding integrity is rather “un-Pythonic” as Python development generally assumes all developers “are adults” and will understand and respect the code they are interacting with. This “development style” generally places more emphasis on evaluating the contents of attributes and parameters when they are about to be used, rather than trying to prevent them from being assigned the wrong type of data in the first place.

There are some sophisticated ways to address this type of data validation issue, but we are going to keep it as simple as possible while still meeting our requirements. We will leave the container object “as-is” and simply add a validation method to the Music class which our view code can call before it adds a new performance to the container. This will also allow us to unit test our validation code.

Remember also, in the Phase 2 deliverables, we will be adding more sophisticated user interface controls. At that point, dates will be entered using some sort of date-picker control and it will be less likely that an invalid date will be entered by a user.

Creating Tests

We will start with some unit tests following the DocTest style. A tutorial on testing is available in the Grok documentation.

We will call our validation method “IsValidKey” and modify the source code at ~/grok/virtualgrok/music/src/music/app.py to read as follows:

import grok
class Music(grok.Application, grok.Container):
    def IsValidKey(self, keyVal):
        pass
class Index(grok.View):
    pass # see app_templates/index.pt

We do this so that the method exists for our tests to call. We don’t want to write the code needed to satisfy our requirements yet. First we want to create some tests based on the requirements.

Create a new directory in the source code directory called “app_tests”. We will use this to store all of our unit tests. (They can be located anywhere in the project source code and the test runner will find them. However, we will put them here in order to keep our project organized.)

cd ~/grok/virtualgrok/music/src/music/
mkdir app_tests
cd app_tests

Using your editor, create a file in this directory called “music.txt” with the following contents:

Tests for the Musical Performance Application.
**********************************************
:Test-Layer: unit

The Music class is a container that will hold all of the information
about "Performances".

When you create a new application instance there should be no objects
in the container::

   >>> from music.app import Music
   >>> performances = Music()
   >>> list(performances.keys())
   []


We are not going to test the pre-existing functionality of a grok.Container.
(That is already handled in its own unit tests.)
However, we do have application requirements that affect the format of
the key used to store performances in the container.

The IsValidKey function needs to return "True" only when a valid string
key of the form "yyyy-mm-dd" with an optional "-hh" suffix is provided.

First we check at the basic string format::

   >>> performances.IsValidKey(None)
   False
   >>> performances.IsValidKey('')
   False
   >>> performances.IsValidKey(0)
   False
   >>> performances.IsValidKey('Not Right')
   False
   >>> performances.IsValidKey('4/1/05')
   False
   >>> performances.IsValidKey('4/1/2005')
   False
   >>> performances.IsValidKey('2008.12.31')
   False
   >>> performances.IsValidKey('2008-3-15')
   False
   >>> performances.IsValidKey('08-3-5')
   False
   >>> performances.IsValidKey('2009--0315')
   False
   >>> performances.IsValidKey('2009-03-15')
   True

Strings that are formatted correctly should fail, if the date
is not valid::

   >>> performances.IsValidKey('2009-13-01')
   False
   >>> performances.IsValidKey('2009-03-32')
   False
   >>> performances.IsValidKey('2009-02-29')
   False

The suffix needs to be a dash, followed by an integer from 00 to 23::

   >>> performances.IsValidKey('2009-03-15Bad')
   False
   >>> performances.IsValidKey('2009-03-15-')
   False
   >>> performances.IsValidKey('2009-03-15-1')
   False
   >>> performances.IsValidKey('2009-03-15-00')
   True
   >>> performances.IsValidKey('2009-03-15-23')
   True
   >>> performances.IsValidKey('2009-03-15-24')
   False

Now, we can run the application tests as we did in the previous section and see the results. Of course, everything will fail.

We can now begin implementing the IsValidKey method in app.py, until all of the tests pass. There are many ways to accomplish this, so feel free to use your own implementation. You can always re-factor it later.

Here is a sample implementation:

import grok
from datetime import datetime
import time

class Music(grok.Application, grok.Container):
    def IsValidKey(self, keyVal):
        if not isinstance(keyVal, basestring):
            return False
        if len(keyVal) in [10, 13]:
            keyParts = keyVal.strip().split('-')
            if len(keyParts) in [3, 4]:
                try:
                    datePart = '-'.join([keyParts[0], keyParts[1], keyParts[2]])
                    newDate = datetime(*(time.strptime(datePart, '%Y-%m-%d')[0:6]))
                except ValueError:
                    return False

                if len(keyParts) == 4:
                    try:
                        newTime = int(keyParts[3])
                        if newTime < 0 or newTime > 23:
                            return False
                    except ValueError:
                        return False

                # Everything checks out.
                return True

        return False

class Index(grok.View):
    pass # see app_templates/index.pt

If you run the application tests again, everything should pass and you should get a result that looks like the following:

Running tests at level 1
Running unit tests:
  Running:
....
  Ran 4 tests with 0 failures and 0 errors in 0.011 seconds.

Next, we will work on creating the performance object and implementing a mechanism to add them to the container.