Kay 0.10.0 RC1でユニットテストを行う方法

Kay 0.10.0 RC1でユニットテストを行う方法 | 山本隆の開発日誌」より。

Google App Engineフレームワーク Kay のバージョン 0.10.0 RC1が公開されています。

Kay 0.10.0 RC1でユニットテストを行う方法を紹介します。

(1) unittest.TestCase から GAETestBase への変更

テストを行うためのフレームワークが unittest.TestCase から GAETestBase へ変更されました。

GAETestBaseの導入によって、コマンドラインによるテスト以外にもブラウザで「http://localhost:8080/_ah/test」にアクセスすることでテストを実行できるようになりました。

また、ローカル環境だけでなく本番環境でテストすることができます。

参考:GoogleAppEngineの強力なテストツールGAETestBaseがKayに標準添付された

以前の方法

import unittest
class ModelTest(unittest.TestCase):

新しい方法

from kay.ext.testutils.gae_test_base import GAETestBase
class ModelTest(GAETestBase):

(2) CLIでビューのテストを行うときは、「manage.py preparse_apps」が必要

RC1ではCLIでビューのテストを行うときは、「manage.py preparse_apps」が必要です。

「manage.py preparse_apps」を実行しないと、「TemplateNotFound」のエラーになります。

python manage.py preparse_apps
python manage.py test

正式版では修正されている可能性があります。

(3) KIND_NAME_UNSWAPPEDをTrueにする

unittestで正常に動作していたテストがGAETestCaseに変更して動かなくなった場合、「KIND_NAME_UNSWAPPED」属性をTrueにすると、動作する可能性が高いです。

GAETestCaseではデフォルトでkindを入れ替えます。

KIND_NAME_UNSWAPPEDをTrueにすることで、この処理を行いません。

class MyappTestCase(GAETestBase):
KIND_NAME_UNSWAPPED = True

正式版では修正されている可能性があります。

(4) CLIによるテストでdb_hookを有効にする

以下のコードのようにsetUp()でフックを登録することで、CLIでテストしたときもdb_hookが機能しました。

class ModelTest(GAETestBase):
KIND_NAME_UNSWAPPED = True
def setUp(self):
from google.appengine.api import apiproxy_stub_map
from kay.utils.db_hook import post_hook
from kay.utils.db_hook import pre_hook
apiproxy_stub_map.apiproxy.GetPostCallHooks().Clear()
apiproxy_stub_map.apiproxy.GetPreCallHooks().Clear()
apiproxy_stub_map.apiproxy.GetPostCallHooks().Append('post_hook', post_hook, 'datastore_v3')
apiproxy_stub_map.apiproxy.GetPreCallHooks().Append('pre_hook', pre_hook, 'datastore_v3')

サンプルコード

以下は、Kay 0.10.0 RC1でチュートリアルのプロジェクトをテストするコードです。

# -*- coding: utf-8 -*-

#import unittest
from google.appengine.ext import db
from werkzeug import BaseResponse, Client
from kay.app import get_application
from kay.utils.test import (
init_recording, get_last_context, get_last_template
)
from myapp.models import (Comment, Category)
from kay.ext.testutils.gae_test_base import GAETestBase

#class ModelTest(unittest.TestCase):
class ModelTest(GAETestBase):
KIND_NAME_UNSWAPPED = True
def setUp(self):
from google.appengine.api import apiproxy_stub_map
from kay.utils.db_hook import post_hook
from kay.utils.db_hook import pre_hook
apiproxy_stub_map.apiproxy.GetPostCallHooks().Clear()
apiproxy_stub_map.apiproxy.GetPreCallHooks().Clear()
apiproxy_stub_map.apiproxy.GetPostCallHooks().Append('post_hook', post_hook, 'datastore_v3')
apiproxy_stub_map.apiproxy.GetPreCallHooks().Append('pre_hook', pre_hook, 'datastore_v3')

def tearDown(self):
db.delete(Comment.all().fetch(10))

def test_model(self):
c = Comment(body=u'Hello Test!')
c.put()
comments = Comment.all().fetch(100)
self.assertEquals(len(comments), 1)
self.assertEquals(comments[0].body, 'Hello Test!')

def test_db_hook(self):
category = Category(name='Category')
category.put()
self.assertEquals(Category.all().count(100), 1)

Comment(body='Body', category=category).put()
self.assertEquals(Comment.all().count(100), 1)

db.delete(category)
#カテゴリーの削除に成功した
self.assertEquals(Category.all().count(100), 0)
#db_hookによりコメントをも削除されている
self.assertEquals(Comment.all().count(100), 0)

#class MyappTestCase(unittest.TestCase):
class MyappTestCase(GAETestBase):
KIND_NAME_UNSWAPPED = True

def setUp(self):
init_recording()
app = get_application()
self.client = Client(app, BaseResponse)

def tearDown(self):
db.delete(Category.all().fetch(10))
db.delete(Comment.all().fetch(10))

def test_post(self):
response = self.client.get('/')
used_template = get_last_template()
used_context = get_last_context()
csrf_token = used_context['form'].csrf_token
response = self.client.post('/', data={'body': 'Hello',
'_csrf_token': csrf_token},
follow_redirects=True)
comments = Comment.all().fetch(100)
self.assertEquals(len(comments), 1)

def test_db_hook(self):
category = Category(name='Category')
category.put()
self.assertEquals(Category.all().count(100), 1)

Comment(body='Body', category=category).put()
self.assertEquals(Comment.all().count(100), 1)

url = '/category/delete/%s' % category.key()
response = self.client.get(url)
self.assertEquals(response.status_code, 302)
#カテゴリーの削除に成功した
self.assertEquals(Category.all().count(100), 0)
#db_hookによりコメントをも削除されている
self.assertEquals(Comment.all().count(100), 0)