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¶m1=value1¶m2=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$
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