Introduction
29/02/2010: I've gone a different direction than what is detailed here, instead I am using the approach taken by the bridal-demo. It wasn't too much trouble to plug a repoze.bfg application into the demo code. Hopefully I will write up a post about how I did that in the near future.
I've come back to a google appengine application that I'd put aside for a while. I've been using repoze.bfg in other work so was determined to do the same here. There is a good tutorial for getting a repoze.bfg application running on gae using appengine-monkey which sets up a virtualenv within which the application can be developed. Great, I'm a fan of virtualenv. A point to remember is that dev_appserver cannot be run with the `virtualev`/bin/python. I would forget that; so now I use a `start.sh` script to run the application.start.sh:
#!/bin/bash PYTHON=/opt/local/bin/python2.5 GAE_PATH=/usr/local/google_appengine $PYTHON $GAE_PATH/dev_appserver.py ./
Testing
I found it surprisingly difficult to set up a test environment for gae. My requirements were firstly to be able to use doctests, secondly to have coverage. Coverage comes with nose to I generally use nose for testing. Initially it seemed that `virtualenv`/bin/easy_install nose coverage would do the job until I came to testing models. No google.appengine, no datastore. NoseGAE seemed to be a good answer but wouldn't work well within the virtualenv (again import problems). I also looked at gaeunit but I can't see any benefits to running tests through the browser during development (I rarely even look at a web application that I'm developing - that's what tests are for, to save me the trouble).However code from gaeunit helped me a lot from which, along with this post, I developed my own solution.
First part of the problem was to get sys.path set up correctly. I took what I needed from runner.py and dev_appserver.py to get the sys.path in working order.
Secondly, I needed to have a test datastore available for testing models, I took a stanza from Dom's post (also used by gaeunit).
Thirdly, when I put() a model an error would be raised about a lack of `app_id` so I solved that with a stanza from $GAE_PATH/tools/dev_appserver.py to read app.yaml and set os.environ['APPLICATION_ID'].
The resulting files then are
test.sh
#!/bin/bash PYTHON=/opt/local/bin/python2.5 $PYTHON testrunner.py $@
testrunner.py
#!/usr/bin/env python # a special test runner import sys import os # {{ the first stanza copied from $GAE_PATH/dev_appserver.py GAE_PATH = "/usr/local/google_appengine" DIR_PATH = GAE_PATH SCRIPT_DIR = os.path.join(DIR_PATH, 'google', 'appengine', 'tools') EXTRA_PATHS = [ DIR_PATH, os.path.join(DIR_PATH, 'lib', 'antlr3'), os.path.join(DIR_PATH, 'lib', 'django'), os.path.join(DIR_PATH, 'lib', 'webob'), os.path.join(DIR_PATH, 'lib', 'yaml', 'lib'), ] sys.path = EXTRA_PATHS + sys.path # }} # {{ and the second from runner.py here = os.path.dirname(__file__) fixpaths = os.path.join(here, 'fixpaths.py') execfile(fixpaths) # }} from google.appengine.api import apiproxy_stub_map from google.appengine.api import datastore_file_stub from google.appengine.tools.dev_appserver import ReadAppConfig # read application config file appinfo_path = os.path.join(here, 'app.yaml') config = ReadAppConfig(appinfo_path) # set application id so that objects can be stored in test database os.environ['APPLICATION_ID'] = config.application def main(module=None): original_apiproxy = apiproxy_stub_map.apiproxy try: apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap() temp_stub = datastore_file_stub.DatastoreFileStub('TestDataStore', None, None, trusted=True) apiproxy_stub_map.apiproxy.RegisterStub('datastore', temp_stub) # Allow the other services to be used as-is for tests. for name in ['user', 'urlfetch', 'mail', 'memcache', 'images']: apiproxy_stub_map.apiproxy.RegisterStub(name, original_apiproxy.GetStub(name)) from nose.core import TestProgram testprogram = TestProgram(module=module) except: apiproxy_stub_map.apiproxy = original_apiproxy # accept module name to pass to test runner try: module = sys.argv[1] except: module = None if __name__ == "__main__": main(module=module)
And for completeness here is setup.cfg
[nosetests] verbose=1 nocapture=1 with-coverage=1 cover-erase=1 cover-inclusive=1 cover-package=mybfgapp
So far, so good. I do rather suspect that this remains incomplete.
References:
http://docs.repoze.org/bfg/1.2/tutorials/gae/index.htmlhttp://blog.appenginefan.com/2008/06/unit-tests-for-google-app-engine-apps.html
http://www.cuberick.com/2008/11/unit-test-your-google-app-engine-models.html
http://code.google.com/p/nose-gae/
http://code.google.com/p/gaeunit/
http://domderrien.blogspot.com/2009/01/automatic-testing-of-gae-applications.html
1 comment:
Very interesting. I keep falling back to gaeunit on projects because it is just so easy to drop in to a new project and validate all the models and logic. I run into the limitation when I start wanting code coverage and a few other things. I've tried twice to get app-engine-patch, django, nose, and nose-gae all working nicely together but each time a few small versioning-related issues have turned the effort into a time sink. So I keep falling back to simple gaeunit and living with what I can get there.
Thanks for the detailed write-up. This almost looks simple enough to give nose another try.
Post a Comment