NoseGAE: nose plugin for Google App Engine testing
Project description
Overview
NoseGAE is a nose plugin that makes it easier to write functional and unit tests for Google App Engine applications.
What does it do?
NoseGAE sets up the GAE development environment before your test run. This means that you can easily write functional tests for your application without having to actually start the dev server and test over http. The plugin also configures and initializes a TestBed instance, your application_id based off your app.yaml, and sets the proper paths using dev_appserver.fix_sys_path().
Functional tests
Consider the simple hello world application in examples/helloworld:
import webapp2
from jinja2 import Environment
class Hello(webapp2.RequestHandler):
def get(self):
self.response.headers['Content-Type'] = 'text/plain'
env = Environment()
template = env.from_string("Hello {{ greeting }}!")
self.response.out.write(template.render(greeting='world'))
app = webapp2.WSGIApplication([('/', Hello)], debug=True)
And a simple functional test suite examples/helloworld/test.py for the application:
from webtest import TestApp
import unittest
import helloworld
app = TestApp(helloworld.app)
class HelloWorldTest(unittest.TestCase):
def test_index(self):
"""Tests that the index page for the application
The page should be served as: Content-Type: text/plain
The body content should contain the string: Hello world!
"""
response = app.get('/')
self.assertEqual(response.content_type, 'text/plain')
self.assertIn('Hello world!', response.body)
Without NoseGAE, the test fails.
helloworld$ nosetests --logging-level=ERROR E ====================================================================== ERROR: Failure: ImportError (No module named webapp2) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/Josh/Developer/Github/jj/lib/python2.7/site-packages/nose-1.3.4-py2.7.egg/nose/loader.py", line 414, in loadTestsFromName addr.filename, addr.module) File "/Users/Josh/Developer/Github/jj/lib/python2.7/site-packages/nose-1.3.4-py2.7.egg/nose/importer.py", line 47, in importFromPath return self.importFromDir(dir_path, fqname) File "/Users/Josh/Developer/Github/jj/lib/python2.7/site-packages/nose-1.3.4-py2.7.egg/nose/importer.py", line 94, in importFromDir mod = load_module(part_fqname, fh, filename, desc) File "/Users/Josh/Developer/Github/nosegae/examples/helloworld/test.py", line 2, in <module> import helloworld File "/Users/Josh/Developer/Github/nosegae/examples/helloworld/helloworld.py", line 1, in <module> import webapp2 ImportError: No module named webapp2 ---------------------------------------------------------------------- Ran 1 test in 0.075s FAILED (errors=1)
With NoseGAE, they pass.
helloworld$ nosetests --logging-level=ERROR --with-gae . ---------------------------------------------------------------------- Ran 1 test in 0.264s OK
Unit tests
Functional tests are only one kind of test, of course. What if you want to write unit tests for your data models? Normally, you can’t use your models at all outside of the dev environment, because the Google App Engine datastore isn’t available. However, since the NoseGAE plugin sets up the development environment around your test run, you can use models directly in your tests.
Consider the examples/pets/models.py file that includes some doctests:
from google.appengine.ext import ndb
class Pet(ndb.Model):
"""The Pet class provides storage for pets.
>>> # initialize testbed stubs
>>> testbed.init_memcache_stub()
>>> testbed.init_datastore_v3_stub()
You can create a pet:
>>> muffy = Pet(name=u'muffy', type=u'dog', breed=u"Shi'Tzu")
>>> muffy # doctest: +ELLIPSIS
Pet(name=u'muffy', type=u'dog', breed=u"Shi'Tzu", ...)
>>> muffy_key = muffy.put()
Once created, you can load a pet by its key:
>>> muffy_key.get() # doctest: +ELLIPSIS
Pet(name=u'muffy', type=u'dog', breed=u"Shi'Tzu", ...)
Or by a query that selects the pet:
>>> list(Pet.query(Pet.type == 'dog')) # doctest: +ELLIPSIS
[Pet(name=u'muffy', ...)]
To modify a pet, change one of its properties and ``put()`` it again.
>>> muffy_2 = muffy
>>> muffy_2.age = 10
>>> muffy_key_2 = muffy_2.put()
The pet's key doesn't change when it is updated.
>>> bool(muffy_key == muffy_key_2)
True
"""
name = ndb.StringProperty(required=True)
type = ndb.StringProperty(required=True, choices=("cat", "dog", "bird", "fish", "monkey"))
breed = ndb.StringProperty()
age = ndb.IntegerProperty()
comments = ndb.TextProperty()
created = ndb.DateTimeProperty(auto_now_add=True, required=True)
def __repr__(self):
return ("Pet(name=%r, type=%r, breed=%r, age=%r, "
"comments=%r, created=%r)" %
(self.name, self.type, self.breed, self.age,
self.comments, self.created))
Without NoseGAE, the doctests fail.
pets$ nosetests --with-doctest --logging-level=ERROR F ====================================================================== FAIL: Doctest: models.Pet ---------------------------------------------------------------------- Traceback (most recent call last): File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/doctest.py", line 2201, in runTest raise self.failureException(self.format_failure(new.getvalue())) AssertionError: Failed doctest test for models.Pet File "/Users/Josh/Developer/Github/nosegae/examples/pets/models.py", line 4, in Pet ---------------------------------------------------------------------- File "/Users/Josh/Developer/Github/nosegae/examples/pets/models.py", line 15, in models.Pet Failed example: muffy_key = muffy.put() Exception raised: Traceback (most recent call last): File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/doctest.py", line 1289, in __run compileflags, 1) in test.globs File "<doctest models.Pet[2]>", line 1, in <module> muffy_key = muffy.put() File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/ext/ndb/model.py", line 3379, in _put return self._put_async(**ctx_options).get_result() File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/ext/ndb/tasklets.py", line 325, in get_result self.check_success() File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/ext/ndb/tasklets.py", line 368, in _help_tasklet_along value = gen.throw(exc.__class__, exc, tb) File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/ext/ndb/context.py", line 810, in put key = yield self._put_batcher.add(entity, options) File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/ext/ndb/tasklets.py", line 371, in _help_tasklet_along value = gen.send(val) File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/ext/ndb/context.py", line 343, in _put_tasklet keys = yield self._conn.async_put(options, datastore_entities) File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/datastore/datastore_rpc.py", line 1801, in async_put return make_put_call(base_req, pbs, extra_hook) File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/datastore/datastore_rpc.py", line 1784, in make_put_call service_name=self._api_version) File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/datastore/datastore_rpc.py", line 1310, in _make_rpc_call rpc = self._create_rpc(config, service_name) File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/datastore/datastore_rpc.py", line 1205, in _create_rpc rpc = apiproxy_stub_map.UserRPC(service_name, deadline, callback) File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/api/apiproxy_stub_map.py", line 414, in __init__ self.__rpc = CreateRPC(service, stubmap) File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/api/apiproxy_stub_map.py", line 68, in CreateRPC assert stub, 'No api proxy found for service "%s"' % service AssertionError: No api proxy found for service "datastore_v3"
With NoseGAE, they pass.
pets$ nosetests --with-doctest --with-gae . ---------------------------------------------------------------------- Ran 1 test in 0.228s OK
Configuring the TestBed
NoseGAE automatically configures your TestBed instance for you and then enables any stubs that you may need for your tests to pass. There are two ways to enable and configure your stubs. The first and most flexible way is to directly initialize the stub(s) on the TestBed instance injected into your test case. The second, and simpler way, is to set the nosegae_* and optional nosegae_*_kwargs attributes on your test case and let NoseGAE configure them for you. The following three sections describe how to use the TestBed in your own tests.
Class Based Tests
The simplest of use cases is when your test class extends unittest.TestCase. The NoseGAE plugin injects an attribute named testbed to the instance of your test class and configures it based upon any attributes matching the convention nosegae_<stubname> and nosegae_<stubname>_kwargs.
This test uses the assigned testbed attribute to manually configure each test.
class MyTest(unittest.TestCase):
def test_using_memcache(self):
"""Unit test using memcache"""
from google.appengine.api import memcache
self.testbed.init_memcache_stub()
memcache.set('test', True, 30)
self.assertTrue(memcache.get('test'))
def test_using_taskqueue(self):
"""Unit test using the taskqueue"""
self.testbed.init_taskqueue_stub(root_path='/path/to/app')
from google.appengine.api import taskqueue
task_url = '/some/task'
taskqueue.add(url=task_url)
stub = self.testbed.get_stub('taskqueue')
tasks = self.taskqueue_stub.get_filtered_tasks(url=task_url)
self.assertEqual(1, len(tasks))
self.assertEqual(task_url, tasks[0].url)
The following test case shows how to write a test that uses the datastore stub based on the simple configuration method using nosegae_<stubname> and nosegae_<stubname>_kwargs.
class DataTest(unittest.TestCase):
# enable the datastore stub
nosegae_datastore_v3 = True
def test_get_entity(self):
"""Naively tests that we can fetch an entity from the datastore"""
entity = MyModel.query().get()
self.assertIsNotNone(entity)
Function Tests
This test case uses the testbed instance assigned to the function to manually configure any needed stubs. See examples/function_manual_config.
def test_index():
# test_index.testbed is assigned by the NoseGAE plugin
test_index.testbed.init_taskqueue_stub(task_retry_seconds=42, root_path=os.path.dirname(__file__))
# Assume the `/` route fires off a task queue and should pass without exceptions
app = TestApp(helloworld.app)
response = app.get('/')
assert 'Hello world!' in str(response)
The following test shows how to use the simple method while passing kwargs to the taskqueue stub’s initialization method. See examples/issue42_task-queue for full example code.
def test_index():
# Assume the `/` route fires off a task queue and should pass without exceptions
app = TestApp(app)
response = app.get('/')
assert 'Hello world!' in str(response)
# Enable any stubs needed as attributes off of the test function
# NoseGAE looks for queue.yaml in the root of the
# application when nosegae_taskqueue is True
test_index.nosegae_taskqueue = True
# ...or you can manually set the path and any additional arguments with the kwargs attribute
test_index.nosegae_taskqueue_kwargs = dict(task_retry_seconds=42, root_path=os.path.dirname(__file__))
Doctest
Doctests are a whole other beast. They still work but all TestBed configuration has to be done manually. NoseGAE uses the nose doctest plugin to inject a global variable named testbed into your doctest scope that contains the current active TestBed instance. See examples/pets/models.py for full example.
class Pet(ndb.Model):
"""The Pet class provides storage for pets.
>>> # Initialize stubs using the injected testbed instance
>>> testbed.init_memcache_stub()
>>> testbed.init_datastore_v3_stub()
You can create a pet:
>>> muffy = Pet(name=u'muffy', type=u'dog', breed=u"Shi'Tzu")
>>> muffy_key = muffy.put()
"""
name = ndb.StringProperty(required=True)
type = ndb.StringProperty(required=True, choices=("cat", "dog", "bird", "fish", "monkey"))
breed = ndb.StringProperty()
Changes in 0.5.2
The 0.5.2 release introduces preliminary modules support by allowing multiple yaml or paths sent to the --gae-application command line option.
nosetests --with-gae \
--gae-application='app.yaml,mobile_frontend.yaml,static_backend.yaml,dispatch.yaml'
Changes in 0.4.0
The 0.4.0 release is a major rewrite to support dev_appserver2. This release introduced two important changes listed below.
Sandbox is gone
Due to changes in the sandboxing mechanisms in dev_appserver2, it isn’t possible for NoseGAE to simulate the deployed environment any longer. The sandboxing feature had to be removed since there is no longer any way to toggle it between noses own internal workings.
This means that certain tests may pass locally but the code in question will fail in production due to restricted modules and functions. As of now there is no workaround but pull requests are welcome!
Testbed is set up for you
The new plugin automatically sets up the initial google.appengine.ext.testbed.Testbed instance and injects it into your test cases for you.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
File details
Details for the file NoseGAE-0.5.4.tar.gz
.
File metadata
- Download URL: NoseGAE-0.5.4.tar.gz
- Upload date:
- Size: 9.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | a28ee025e635199a0d09da17e462cf534ad02e6715662b0451dd56e3ccef8530 |
|
MD5 | d764797aaece959c53bdefc6ccce5a4b |
|
BLAKE2b-256 | 569b521242aa81434175c2d131fda506cb51b6e5e3f8043e0ee0679576d3ba1f |