Saturday, November 29, 2008

Unit Test Your Google App Engine Models


I've been working on a project using Google App Engine (GAE) called "les Freres Jacques" That manipulates images of people's face onto the cover of this old french LP. Look for it to be out in a couple months. Anyway, My favorite way to start any project is by doing TDD. Unfortunately I'm new to python and GAE simultaneously so I had to do plenty of research to figure out how to unit test a GAE app. Most importantly for me was the ability to test my models that are based on google's datastore api. What follows is some information to get you started writing unit tests against a GAE model. First, a list of the tools you need to install.
  • Nose is a tool for running your python unit tests.
  • NoseGAE is a plugin for nose that bootstraps the GAE environment.
The easiest way to install these is with python's easy_install, which as far as I can tell is similar to ruby's 'gem' program and perl's 'cpan' program... though I don't know if it resolves dependencies automatically. Anyway, on OSX easy_install is installed by default so you can simply type
sudo easy_install nose
sudo easy_install nosegae
Now let's create a test to exercize a simple GAE model object. Here is a file called test_simple_model.py
import unittest
from google.appengine.api.users import User
from test_example.simple_model import SimpleModel

class TestSimpleModel(unittest.TestCase):
def test_creation(self):
user = User(email = "test@foo.com")
model = SimpleModel(goo_user = user)
model.put()
fetched_model = SimpleModel.all().filter('goo_user =', user).fetch(1)[0]
self.assertEquals(fetched_model.goo_user, user)
The nose tool we installed earlier gives us a program called nosetests to run. When you call it it looks through your project and runs all your tests. We should call it now with the google app engine switch.
nosetests --with-gae
Wheee!! It is a lovely failing test.

======================================================================
ERROR: Failure: ImportError (No module named simple_model)
----------------------------------------------------------------------
Traceback (most recent call last):
...
from test_example.simple_model import SimpleModel
ImportError: No module named simple_model

----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (errors=1)
Good. Now we need to write some code to get our test passing. Here is what I wrote in a file called simple_model.py.
from google.appengine.ext import db
class SimpleModel(db.Model):
goo_user = db.UserProperty()
Now when I run
nosetests --with-gae
I get the lovely
.
----------------------------------------------------------------------
Ran 1 test in 0.008s

OK
and I am happy because I see how I can do TDD with GAE! Here is a list of references I used to figure this stuff out. Hope you find them useful. You can find the full source of this example here.
Update: 11/30/08
The datastore persists between tests which isn't usually what I want to happen. I submitted an issue on the nose-gae issue tracker. In the meantime here is a workaround to make sure the datastore is flushed between runs. Add this method to your test class and call it in your setUp method.

from google.appengine.api import apiproxy_stub_map
from google.appengine.api import datastore_file_stub

def clear_datastore(self):
# Use a fresh stub datastore.
apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
stub = datastore_file_stub.DatastoreFileStub('appid', '/dev/null', '/dev/null')
apiproxy_stub_map.apiproxy.RegisterStub('datastore_v3', stub)
Update: 1/29/09
Reading Dom's well researched and documented post on testing App Engine applications. I thought I better spruce up my own post by adding a citation. The code from the clear_datastore method above comes from this message posted on the google app engine google groups mailing list.
Update: 5/17/09
There is currently an issue with nosegae. See defect 18. There are patches that fix the issue posted there. I downloaded the source code, removed the subversion directories, patched the code and ran
easy_install .
in the root directory of the code. That fixed the issue for me.
Update: 5/17/09
As of GAE SDK 1.2.1 the appid of your datastore stub must match your appID. Make sure your call to DatastoreFileStub uses your actual app id.

6 comments:

marram said...

Dude!. Thanks for this post. I've been scratching my head for the last two hours trying to figure out why entities are persisting between tests.

Thanks!

Dom Derrien said...

Hi Josh,

Thanks for your post. It gave me materials to write an extended post. I even mentioned you're a TDD adopter ;)


A+, Dom
--
http://domderrien.blogspot.com/2009/01/automatic-testing-of-gae-applications.html

DF said...

Why does nosetests not find my test?

$ nosetests
----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

$ cat unittests.py
import unittest
from google.appengine.api.users import User
from models import Poll, Option

class TestPoll(unittest.TestCase):
def test_creation(self):
poll = Poll()
poll.put()
self.assertTrue(poll.id() != 0)
option = Option()
option.poll = poll
option.text = "some option text"
option.put()
self.assertEquals(1, len(poll.get_options()))
self.assertEquals(0, len(poll.get_responses()))

marram said...

Your test script file name should have a test_ prefix.

DF said...

Bless you!

If only http://somethingaboutorange.com/mrl/projects/nose/0.11.1/finding_tests.html

had anything nearly as informative.

JJ Geewax said...

You may also be interested in GAE Testbed (http://gae-testbed.googlecode.com) which provides a few base test cases that simplify testing things like sending emails or adding things to the Task Queue.