19
WebTest で Web API でででで & Python でででででででででで でででででででで でででで Shizuoka.py #6

Shizuoka.py #6 WebTestでWeb APIのテスト & Pythonメタプログラミングでテストの自動生成

  • Upload
    nao-oec

  • View
    136

  • Download
    2

Embed Size (px)

Citation preview

WebTestでWeb APIのテスト& Pythonメタプログラミングでテストの自動生成オーイシShizuoka.py #6

Web APIを何故テストしたいの?

• データアーカイブを検索して再利用するためのラッパー APIを構築しています。• 対象のデータベースが増える&検索対象のフィールドが増えると API が継ぎ足される感じで増えて行く。• 仕様のアップデートで動作の影響が無いか検証する必用がある。• API が増えるとテストが大変。

WebTestとは

• WSGI アプリのテストを HTTP サーバを起動せずに行うことができる。※実サーバを使ったテストもできます。• Pylons ( Rails 的な Web アプリケーションフレームワーク)のパッケージの一部として開発されている。• インストールは ” pip install WebTest” で OK• http://docs.pylonsproject.org/projects/webtest/en/latest/

WebTestの簡単な例from webtest import TestApp

def application(environ, start_response): body = 'たのしー '.encode('utf-8') headers = [('Content-Type', 'text/html; charset=utf8'), ('Content-Length', str(len(body)))] start_response('200 OK', headers) return [body]

app = TestApp(application)resp = app.get('/')

assert resp.content_type == 'text/html'assert resp.content_length > 0assert 'たのしー ' in resp

#assert 'すごーい ' in resp

# このままだと実行しても何も起きないが# assert 'すごーい ' in resp をアンコメントすると>Traceback (most recent call last):> File "test_webtest_basic.py", line 16, in <module>> assert 'すごーい ' in resp>AssertionError

unittestと一緒に使うことが多いのかも• assertEqual(a, b)• assertTrue(x)• assertFalse(x)• assertGreater(a, b)• assertIn(a, b)• assertIs(a, b)• assertRaises• 等々 unittest のアサートメソッドを利用します(バージョンによって利用出来るメソッドは異なる)

実サーバのテストのサンプルos.environ['WEBTEST_TARGET_URL'] = 'http://target_api_address'app = TestApp(index)#または app = TestApp('http://target_api_address')

class ApiTest(unittest.TestCase): def test_api_root(self): res = app.get('/') self.assertEqual(res.status, '200 OK') self.assertEqual(res.content_type, 'application/json')

def test_get_ids(self): res = app.get('/bioproject?organism_name=Papilio%20xuthus') self.assertGreater(res.json['numFound'], 1)

def test_get_metadata(self): res = app.get('/bioproject/PRJNA25') keys = res.json.keys() self.assertIn("Package", keys)

def test_post_ids(self): res = app.post_json('/bioproject', {"ids": ["PRJNA26","PRJNA25"]}) res_len = len(list(res.json)) self.assertEqual(res_len, 2)

if __name__ == '__main__': unittest.main()

WebTest +unittestの結果# このままテストがエラーを発生せず完了するとRan 4 tests in 0.898sOK# エラーを発生するよう、テストを変更すると…def test_post_ids(self): res = app.post_json('/bioproject', {"ids": ["PRJNA26", "PRJNA25"]}) res_len = len(list(res.json)) self.assertEqual(res_len, 3)F==================================================================FAIL: test_post_ids (__main__.ApiTest)----------------------------------------------------------------------Traceback (most recent call last): File "test_api.py", line 27, in test_post_ids self.assertEqual(res_len, 3)AssertionError: 2 != 3----------------------------------------------------------------------Ran 4 tests in 0.575sFAILED (failures=1)

テストの自動生成

ドキュメントベースでWeb APIのテストを管理・実施したい。

テストの自動生成パターンを4つ見つけました

1. 動的メソッド定義2. メタクラス3. nose × generator

4. nose_parameterized

動的メソッド定義import unittest

l = [["foo", "a", "a"], ["bar", "a", "b"], ["baz", "b", "b"]]

class TestSequense(unittest.TestCase): pass

def test_gen(a, b): def test(self): self.assertEqual(a, b)

return test

if __name__ == '__main__': for t in l: test_name = 'test_%s' % t[0] test = test_gen(t[1], t[2])

setattr(TestSequense, test_name, test) unittest.main()F..======================================================================FAIL: test_bar (__main__.TestSequense)~Ran 3 tests in 0.001sFAILED (failures=1)

動的メソッド定義

1. 高階関数の test_gen は引数 (a, b) を受け取りアサートメソッドを生成し、 test メソッドを返す。2. setattr() は TestCase に新しい属性を追加する。その属性の値がメソッド名 (=test_name) と関数 (=test) 。3. 1-2 が設定リストの要素で繰り返される。

メタクラスによる動的テストimport unittest

l = [["foo", "a", "b"], ["bar", "a", "b"], ["baz", "a", "a"]]

class TestSequenseMeta(type): def __new__(mcs, cls_name, cls_base, cls_dict): def gen_test(a, b): def test(self): self.assertEqual(a, b) return test

for tname, a, b in l: test_name = "test_{}".format(tname) cls_dict[test_name] = gen_test(a, b) return type.__new__(mcs, cls_name, cls_base, cls_dict)

class TestSequence(unittest.TestCase, metaclass=TestSequenseMeta): pass

if __name__ == '__main__': unittest.main()F..======================================================================~Ran 3 tests in 0.001sFAILED (failures=1)

メタクラスによる動的テスト1. クラス定義は実は typeのインスタンス。 type(クラス名、親クラス , メソッドや属性を定義 )。2. クラス定義にmetaclassが存在すると、クラスを生成する前に

metaclassに格納された関数が typeの代わりに呼び出される。3. typeは __new__メソッドでクラス生成の主な処理を行う。メタクラスを使うとクラス定義時に処理が実行される。4. サンプルでは、クラス生成の前に TestSequenseMeta()が呼び出され、クラス定義が返る

nose × generatorparam_list = [('a', 'a'), ('a', 'b'), ('b', 'b')]

def test_generator(): for params in param_list: yield check_em, params[0], params[1]

def check_em(a, b): assert a == b$ nosetests test_nose_generators.py .F.======================================================================~ File "/Users/../test_nose_generators.py", line 11, in check_em assert a == bAssertionError----------------------------------------------------------------------Ran 3 tests in 0.002s

FAILED (failures=1)

nosetestsコマンドで test_xx.pyなファイルを探して test_な関数を実行してくれる。

nose_parameterizedfrom nose_parameterized import parameterizedimport unittest

class TestSequence(unittest.TestCase): @parameterized.expand( [ ["foo", "a", "b"], ["bar", "b", "b"], ["baz", "a", "a"], ]) def test_sequence(self, name, a, b): self.assertEqual(a,b)

if __name__ == '__main__': unittest.main()FAIL: test_sequence_1_bar (__main__.TestSequence)----------------------------------------------------------------------~ File "test_nose_parameterized.py", line 13, in test_sequence self.assertEqual(a,b)AssertionError: 'a' != 'b'- a+ b----------------------------------------------------------------------Ran 3 tests in 0.001s

FAILED (failures=1)

decoratorの引数でテストの設定を渡す感じ?

動的メソッド定義でWebTestしてみたimport unittestfrom webtest import TestApp

app = TestApp('http://target_api_url')tests = [["bioproject", "/bioproject/PRJNA25", "In", "Package"], ["organism_name", "/bioproject?organism_name=Papilio%20xuthus", "Greater", 1], ["root", "/sra", "Equal", "200 OK”]]

class TestSequense(unittest.TestCase): pass

def test_gen(path, method, b): def Equal(self): res = app.get(path) a = res.status self.assertEqual(a, b)

def Greater(self): res = app.get(path) a = res.json['numFound'] self.assertGreater(a, b)

def In(self): res = app.get(path) a = res.json.keys() self.assertIn(b, a)

#return eval(method) return locals()[method]

if __name__ == '__main__': for name, path, method, val in tests: test_name = 'test_{}'.format(name) test = test_gen(path, method, val) setattr(TestSequense, test_name, test) unittest.main()

-------------------------------------------------------Ran 3 tests in 0.298sOKでした。

- メタプログラミングなのにアドホックすぎ!- さすがにこのままコレがしたいわけではない。- 設定出来る項目が多すぎるのかも。

- テストのルールを整備しかつ大量なテストであれば使えなくは無いかも。

参考サイトHow to generate dynamic (parametrized) unit tests in python?http://stackoverflow.com/questions/32899/how-to-generate-dynamic-parametrized-unit-tests-in-pythonメタプログラミング Pythonhttps://tell-k.github.io/pyconjp2016/#1Python による黒魔術入門http://www.slideshare.net/ssuser38b704/ll-lang-blackmagicPython でメタプログラミングhttp://qiita.com/fujimisakari/items/0d4786dd9ddeed4eb702