May 13, 2010

Introducing GAETestBase

A Japanese highly skilled engineer tago-san wrote an excellent module named GAETestBase. I'd like to introduce GAETestBase to you all. This entry is basically an English translation of this article of his.

GAETestBase is a useful subclass of the standard unittest.TestCase. You can just write your test classes as usual by just extending GAETestBase class for utilizing this modules functionality.

GAETestBase can be downloaded at Google Code Repository.

What can I do with this module?

  • Run your tests via CLI
    • You can run your tests without setting up necessary environments (e.g. stubs,  env, etc..)
    • Tests will be use remote_api connection for accessing production datastore if you configure to do so in your TestCase.
  • Run your tests via GAEUnit
    • GAEUnit is a web-based test runner hosted at: GAEUnit Google Code
    • You can run your test with web-browser in your development environment.
    • You can also run your tests with web-browser in your production environment. The test will be invoked under real production services(not memory based DatastoreFileStub) if you configure to do so.
  • Run your tests with special overridden kind() method.
    • You can use this overridden kind method just in your tests without any changes in your code.
      • For example, entities of  a model class "MyModel" will be stored as kind "t_MyModel".
      • Be careful with this special kind method in your TestCase. Please see an example bellow.

        Key.from_path('MyModel', id)       # NG!
        Key.from_path(MyModel.kind(), id)  # OK!
    • Especially useful with tests via remote_api and tests on production(GAEUnit) because you can run your tests without any data pollution.
    • Of course, you can suppress this behavior by configuration
  • Clean up all the kinds that are used in your tests.
    • You can delete all the kinds used in your test after running your test.
      • Only the kinds which is accessed in a particular TestCase, will be deleted.
      • *accessed* here includes just reading. So if you read a existing kind, the kind will become a target of deletion.
    • CAUTION: This feature could be very dangerous if you disable overridden kind method.

    Let's do it

    Download necessary files and deployment

    You need to do following three things before start writing your tests.
    1. Deploy gaeunit.py
      • Download the file and put it into your project directory.
      • You need to re-write this file for changing _LOCAL_TEST_DIR if your tests are not placed in test/*.py.
    2. Add two entries to your app.yaml
      • Configure a gaeunit handler and remote_api handler as follows:


        - url: /remote_api
          script: $PYTHON_LIB/google/appengine/ext/remote_api/handler.py
          login: admin
        - url: /test.*
          login: admin  # This is important if you deploy the test directory in production!
          script: gaeunit.py

    3. Create "test" directory and put gae_test_base.py into it, and edit constant variables in the file.
      • You need to configure following variables:
        • GAE_HOME: a path for google appengine SDK
        • PROJECT_HOME: a path for your target project
        • APP_ID: your application id
        • REMOTE_API_ENTRY_POINT: a path section of the URI of remote_api (e.g. "/remote_api" for above example).

    Run your test in local env.

    Firstly, let's try running your test for local environment. This is a pretty ordinary test. "test_defs" here is just for model definition, so replace it with actual module with yours.

    from gae_test_base import *
    
    from test_defs import *
    from google.appengine.ext import db
    
    class DummyTest1(GAETestBase):
    
        def test_put(self):
            x1 = XEntity(x="x1")
            k1 = x1.put()
            self.assertEqual(db.get(k1).x, "x1")
    
        def test_tx(self):
            def tx1(x2key, x2, x3):
                x2 = XEntity(key=x2key, x=x2)
                x3 = XEntity(x=x3, parent=x2)
                x2.put()
                x3.put()
                return (x2.key(), x3.key())
            x2k = db.allocate_ids(db.Key.from_path(XEntity.kind(), -1), 1)[0]
            k2, k3 = db.run_in_transaction(tx1, db.Key.from_path(XEntity.kind(), int(x2k)), "x2", "x3")
            self.assertEqual(db.get(k2).x, "x2")
            self.assertEqual(db.get(k3).x, "x3")

    Actual tests are very simple as follows:
    1. test_put
      • Create a new entity and put() it.
      • Getting an entity with returned key again, and compare values of two entities.
    2. test_tx
      • Define a following function "tx1" for transaction
        • receives a key for a root entity, a value for a property of the root entity, and a value for a property of the child entity as its arguments.
        • creates a new root entity and a child entity.
        • puts'em together
        • returns those two keys
      • Get an id for a new root entity by invoking allocate_ids
      • Run tx1 under a transaction
      • Get two entities with the key returned from transaction call, and compare property values with expected one.
    Here is an example output when running this testcase:

    $ python2.5 dummy_test.py 
    ..
    ----------------------------------------------------------------------
    Ran 2 tests in 0.020s
    
    OK
    $

    Here is an example screenshot when running this testcase via GAEUnit:


    Run your test via remote_api or on production

    Next, let's add a new testcase that uses the production datastore as its backend. Here is an example configuration for using remote_api via CLI, and using production datastore when deployed on appspot.

    class DummyTest2(GAETestBase):
        USE_PRODUCTION_STUBS = True
        USE_REMOTE_STUBS = True
    
        def test_put(self):
            y1 = YEntity(y="y1")
            k1 = y1.put()
            self.assertEqual(db.get(k1).y, "y1")
    
        def test_tx(self):
            def tx1(y2key, y2, y3):
                y2 = YEntity(key=y2key, y=y2)
                y3 = YEntity(y=y3, parent=y2)
                y2.put()
                y3.put()
                return (y2.key(), y3.key())
            y2k = db.allocate_ids(db.Key.from_path(YEntity.kind(), -1), 1)[0]
            k2, k3 = db.run_in_transaction(tx1, db.Key.from_path(YEntity.kind(), int(y2k)), "y2", "y3")
            self.assertEqual(db.get(k2).y, "y2")
            self.assertEqual(db.get(k3).y, "y3")

    You can configure all the behavior by defining class attributes on your extended TestCase classes. The default value of all the configurable attributes is False when omitted. This new testcase is almost the same as the previous testcase except for a target model class.

    Here is an example output via CLI:

    $ python2.5 dummy_test.py 
    ....
    ----------------------------------------------------------------------
    Ran 4 tests in 6.578s
    
    OK
    $

    Now you can recognize that, this time, two testcases are called. You may asked your username and password on test that uses remote_api, though its omitted because there is a valid appcfg_cookies.

    Here is an output via GAEUnit:


    You can also run these tests on appspot by just visiting http://app-id.appspot.com/test. After running these tests on appspot, you can see that entities which are used in your test are stored in the production datastore. A kind of these entities is "t_YEntity" (specially prefixed for test) like below:


    Cleaning up entities in your tests

    If you don't like entities created in your tests remains, you can write a testcase which deletes entities which are accessed in your tests like:

    class DummyTest3(GAETestBase):
        USE_PRODUCTION_STUBS = True
        USE_REMOTE_STUBS = True
        CLEANUP_USED_KIND = True
    
        def test_put(self):
            z1 = ZEntity(z="z1")
            k1 = z1.put()
            self.assertEqual(db.get(k1).z, "z1")
    
        def test_tx(self):
            def tx1(z2key, z2, z3):
                z2 = ZEntity(key=z2key, z=z2)
                z3 = ZEntity(z=z3, parent=z2)
                z2.put()
                z3.put()
                return (z2.key(), z3.key())
            z2k = db.allocate_ids(db.Key.from_path(ZEntity.kind(), -1), 1)[0]
            k2, k3 = db.run_in_transaction(tx1, db.Key.from_path(ZEntity.kind(), int(z2k)), "z2", "z3")
            self.assertEqual(db.get(k2).z, "z2")
            self.assertEqual(db.get(k3).z, "z3")

    You can run these test via CLI, via GAEUnit(dev), and via GAEUnit(prod).

    After running your tests, you can see there is no "t_ZEntity" on the datastore at all:


    Per testcase cutomizing

    Here is an example for customizing kind prefix and clean up behavior.


    class DummyTest4(GAETestBase):
        USE_PRODUCTION_STUBS = True
        USE_REMOTE_STUBS = True
        CLEANUP_USED_KIND = False
        KIND_PREFIX_IN_TEST = 'test'
    
        def test_put(self):
            z1 = ZEntity(z="z1")
            k1 = z1.put()
            self.assertEqual(db.get(k1).z, "z1")
    
        def test_tx(self):
            def tx1(z2key, z2, z3):
                z2 = ZEntity(key=z2key, z=z2)
                z3 = ZEntity(z=z3, parent=z2)
                z2.put()
                z3.put()
                return (z2.key(), z3.key())
            z2k = db.allocate_ids(db.Key.from_path(ZEntity.kind(), -1), 1)[0]
            k2, k3 = db.run_in_transaction(tx1, db.Key.from_path(ZEntity.kind(), int(z2k)), "z2", "z3")
            self.assertEqual(db.get(k2).z, "z2")
            self.assertEqual(db.get(k3).z, "z3")

    After running this test, you can see "test_ZEntity" is stored in the datastore(ruled out from deletion).


    Accessing the real data on production


    Here is an example testcase for accessing the real data on prod.


    class DummyTest5(GAETestBase):
        USE_PRODUCTION_STUBS = True
        USE_REMOTE_STUBS = True
        CLEANUP_USED_KIND = False
        KIND_NAME_UNSWAPPED = True
    
        def test_put(self):
            count = PEntity.all().count()
            p1 = PEntity(i=count)
            k1 = p1.put()
            px = db.get(k1)
            self.assertEqual(px.i, count)

    If KIND_NAME_UNSWAPPED is set to True, you can suppress overriding kind method on db.Model. So, if you turn on USE_REMOTE_STUBS or USE_PRODUCTION_STUBS, your tests can access the real data on the production datastore.

    How to use this module with Kay Framework?

    Now Kay has GAETestBase bundled, so you can use it out of the box!
    For more details, please see this article as well.

    Caution

    TODO: translate


    Far beyond

    TODO: translate

    No comments: