Unit Testing with Python and Google App Engine

It came to the point where developing my soon to be web service become too difficult since I have to write test codes on my request handlers. I finally made my way to Python Unit testing using the unittest module.

The project

The project is just a simple Google AppEngine for Python 2.5 where it is suppose to serve web services to my future sites. Think of a single data store for multiple sites, that’s what I’m trying to create. Python actually has the xUnit unit testing module called unittest. This is the python version of those jUnit, PHPUnit and the like.

unittest2 for python 2.5

The unittest for Python 2.7 has many great features such as autodiscover that are not present on old versions. The module unittest2 backported those features to lower Python versions down to 2.4. See the unittest2 project site.

Download unittest2 module and simply include them on the project. All you have to do is replace unittest with unittest2 and refer to the unittest documentation for Python 2.7.

File stucture

My tests are under the test directory below the project root. The files just mimic the directory structure of my main classes with test*.py file naming convention for the files to test.

myproject/
  unittest2/
  myclasses/
    handler/
      __init__.py
      rest.py
    model/
      __init__.py
  test/
    __init__.py
    myclasses/
      __init__.py
      handler/
        __init__.py
        testrest.py
      model/
        __init__.py
  app.yml
  main.py
  test.py
  __init__.py

As you’ve noticed, there are tons of __init__.py files. They are needed to make those directories as packages even those for the tests. For the main classes, of course it is required to make those packages and modules available for the project. For the test classes, they are needed to allow the test runner auto discover test files and run the tests under them.

You may also notice that the project root is also a package. This will allow me to run the test that will properly load the main classes.

The class to test

In our example, we are trying to test a REST handler – the base class for all REST handlers. This class is not yet complete but what we are trying to do is test the initial class methods on it.

File: myclasses/handler/rest.py.
Methods/functions: rest.generate_signature() and rest.RestHandler.is_action_valid()

import hashlib
import hmac

from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app

def generate_signature(key, parameters, sig_key='sig'):
    """Generates signature based on the passed key and parameters"""

    if sig_key in parameters:
        del parameters[sig_key]

    keys = parameters.keys()
    keys.sort()
    msg = ''

    for k in keys:
        msg += k + parameters[k]

    return hmac.new(key, msg, hashlib.sha256).hexdigest()

class RestHandler(webapp.RequestHandler):
    """
    REST handler for rest requests accross the cluster

    /api/soloflight?action=do_stuff&param1=value1&param2=value2&sig=blah
    """

    api_methods = {
        'get': [],
        'post': [],
        'put': [],
        'delete': []
    }

    request_action = None
    request_parameters = {}

    def is_action_valid(self, method, action):
        """
        Returns True if and only if the action is valid for a method
        and is a valid handler method
        """

        if method in self.api_methods:
            if action in self.api_methods[method]:
                if getattr(self, action, None):
                    return True

        return False

The test runner

The test runner is located at the project root named test.py. What it do is simply auto discover test cases and run them. It allows to specify the Google AppEngine SDK path and the target test directory. With its code, it sets the top level directory the same as the project root directory. This way, it can load classes from the myclasses package.

#!/usr/bin/python
import optparse
import sys
# Install the Python unittest2 package before you run this script.
import unittest2

USAGE = """%prog SDK_PATH TEST_PATH
Run unit tests for App Engine apps.

SDK_PATH    Path to the SDK installation
TEST_PATH   Path to package containing test modules"""


def main(sdk_path, test_path):
    sys.path.insert(0, sdk_path)
    import dev_appserver
    dev_appserver.fix_sys_path()
    suite = unittest2.loader.TestLoader().discover(test_path, top_level_dir='.')
    unittest2.TextTestRunner(verbosity=2).run(suite)


if __name__ == '__main__':
    parser = optparse.OptionParser(USAGE)
    options, args = parser.parse_args()
    if len(args) != 2:
        print 'Error: Exactly 2 arguments required.'
        parser.print_help()
        sys.exit(1)
    SDK_PATH = args[0]
    TEST_PATH = args[1]
    main(SDK_PATH, TEST_PATH)

The test

The test is a bit long and I’m trying to look for @dataProvider from PHPUnit but didn’t have time to look for it so I just created a whacky data provider within the test case.

File: test/myclasses/handler/testrest.py

import unittest2
import hmac
import hashlib

from myclasses.handler import rest

class TestRestHandler(unittest2.TestCase):
    def test_rest_object(self):
        r = rest.RestHandler()
        self.assertIsNotNone(r.api_methods)

    def test_methods(self):
        """Assert that all methods are existing on bare rest handler"""
        r = rest.RestHandler()
        methods = ['get', 'post', 'put', 'delete', 'head', 'trace']

        for method in methods:
            self.assertIsNotNone(getattr(r, method, None))

    def test_is_action_valid(self):
        """Assert that an action is valid for the given handler configuration"""
        r = rest.RestHandler()

        input_values = [
            {
                'api_methods': {},
                'input': {
                    'get': ['foo', 'bar'],
                    'post': ['fooo'],
                    'test': ['joe', 'brad']
                },
                'expected': False
            },
            {
                'api_methods': {
                    'get': [],
                    'post': []
                },
                'input': {
                    'get': ['foo', 'bar'],
                    'post': ['fooo'],
                    'test': ['joe', 'brad']
                },
                'expected': False
            },
            {
                'api_methods': {
                    'get': ['xxx', 'yyy', 'zzz'],
                    'post': []
                },
                'input': {
                    'get': ['foo', 'bar'],
                    'post': ['fooo'],
                    'test': ['joe', 'brad']
                },
                'expected': False
            },
            {
                'api_methods': {
                    'get': ['foo', 'bar'],
                    'post': ['fooo'],
                    'test': ['joe', 'brad']
                },
                'input': {
                    'get': ['foo', 'bar'],
                    'post': ['fooo'],
                    'test': ['joe', 'brad']
                },
                'expected': True
            },
        ]

        for test_input in input_values:
            r = rest.RestHandler()
            r.api_methods = test_input['api_methods']

            if 'api_methods' in test_input:
                for api_method, api_actions in test_input['api_methods'].iteritems():
                    setattr(r, method, lambda: 1)
                    for api_action in api_actions:
                        setattr(r, api_action, lambda: 1)

            for method, actions in test_input['input'].iteritems():
                for action in actions:
                    self.assertEquals(test_input['expected'], r.is_action_valid(method, action))

    def test_signature(self):
        """Assert that signature on parameters matched"""

        # Sample signature routine
        msg = ''.join(['action', 
                       'get_recent_promos', 
                       'end_date', 
                       '2012-01-01',
                       'key',
                       '123456',
                       'start_date',
                       '2012-02-14'
                       ])
        signature_sample = hmac.new('123456', msg, hashlib.sha256).hexdigest()

        input_values = [
            {
                'parameters': {
                    'action': 'do',
                    'foo': 'bar',
                    'sig': 'xxxxxxxxxxxxxxx',
                    'key': 'burf'
                },
                'key': 'burf',
                'signature': 'burffailed',
                'expected': False
            },
            {
                'parameters': {
                    'action': 'get_recent_promos',
                    'key': '123456',
                    'start_date': '2012-02-14',
                    'end_date': '2012-01-01',
                    'sig': signature_sample
                },
                'key': '123456',
                'signature': signature_sample,
                'expected': True
            }
        ]

        for test_input in input_values:
            gen_sig = rest.generate_signature(test_input['key'], test_input['parameters'])
            self.assertEquals(test_input['expected'], 
                              test_input['signature'] == gen_sig)

if __name__ == '__main__':
    unittest2.main()

Running the test

To run the test, all we have to do is in the terminal, go to the project root directory and run the test runner. Since I’m using multiple Python versions, I have to specify the Python executable for Python 2.5.

/opt/python-2.5/bin/python test.py /opt/google-appengine/ test

Below is the actual results, too bad there are no colors.

lysender@darkstar:~/www-repo/myproject$ /opt/python-2.5/bin/python test.py /opt/google-appengine/ test
test_is_action_valid (test.myclasses.handler.testrest.TestRestHandler)
Assert that an action is valid for the given handler configuration ... ok
test_methods (test.myclasses.handler.testrest.TestRestHandler)
Assert that all methods are existing on bare rest handler ... ok
test_rest_object (test.myclasses.handler.testrest.TestRestHandler) ... ok
test_signature (test.myclasses.handler.testrest.TestRestHandler)
Assert that signature on parameters matched ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.030s

OK
lysender@darkstar:~/www-repo/myproject$ 
This entry was posted in Python and tagged , , , , , . Bookmark the permalink.

Related Posts

One Response to Unit Testing with Python and Google App Engine

  1. james says:

    Dear Lysender,
    As a beginner, I am trying to find a whold example of how gae unittest should be implemented. The GAE documentation regarding this part does not help. I like your example very much but am still not able to get it setup and run successfully. I know there much be something here or there missing. Would you send me a zip file that includes entire project so that I can replicate it on my machine?
    Thank you so much in advance and happy holidays!
    James

Leave a Reply

Your email address will not be published. Required fields are marked *