Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

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

1,350 views

Published on

2017-02-18 Shizuoka.py #6の発表です。PythonのWSGIアプリのテストツールWebTestを使ったWeb APIのテストについての紹介と、メタプログラミング的なテストの自動生成の簡単な紹介を行っています。

Published in: Technology
  • Be the first to comment

  • Be the first to like this

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

  1. 1. WebTestでWeb APIのテスト & Pythonメタプログラミングで テストの自動生成 オーイシ Shizuoka.py #6
  2. 2. Web APIを何故テストしたいの? • データアーカイブを検索して再利用するためのラッパーAPI を構築しています。 • 対象のデータベースが増える&検索対象のフィールドが増え るとAPIが継ぎ足される感じで増えて行く。 • 仕様のアップデートで動作の影響が無いか検証する必用があ る。 • APIが増えるとテストが大変。
  3. 3. WebTestとは • WSGIアプリのテストをHTTPサーバを起動せずに行うことが できる。※実サーバを使ったテストもできます。 • Pylons(Rails的なWebアプリケーションフレームワーク)の パッケージの一部として開発されている。 • インストールは ”pip install WebTest” でOK • http://docs.pylonsproject.org/projects/webtest/en/latest/
  4. 4. 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 > 0 assert 'たのしー' 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
  5. 5. unittestと一緒に使うことが多いのかも • assertEqual(a, b) • assertTrue(x) • assertFalse(x) • assertGreater(a, b) • assertIn(a, b) • assertIs(a, b) • assertRaises • 等々unittestのアサートメソッドを利用します (バージョンによって利用出来るメソッドは異なる)
  6. 6. 実サーバのテストのサンプル 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()
  7. 7. WebTest +unittestの結果 #このままテストがエラーを発生せず完了すると Ran 4 tests in 0.898s OK # エラーを発生するよう、テストを変更すると… 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.575s FAILED (failures=1)
  8. 8. テストの自動生成
  9. 9. ドキュメントベースでWeb APIのテストを管理・実施したい。
  10. 10. テストの自動生成パターンを4つ見つけました 1. 動的メソッド定義 2. メタクラス 3. nose × generator 4. nose_parameterized
  11. 11. 動的メソッド定義 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.001s FAILED (failures=1)
  12. 12. 動的メソッド定義 1. 高階関数のtest_genは引数(a, b)を受け取りアサートメソッドを 生成し、testメソッドを返す。 2. setattr()はTestCaseに新しい属性を追加する。その属性の値がメ ソッド名(=test_name)と関数(=test)。 3. 1-2が設定リストの要素で繰り返される。
  13. 13. メタクラスによる動的テスト 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.001s FAILED (failures=1)
  14. 14. メタクラスによる動的テスト 1. クラス定義は実はtypeのインスタンス。type(クラス名、親クラス, メソッドや属性を定義)。 2. クラス定義にmetaclassが存在すると、クラスを生成する前に metaclassに格納された関数がtypeの代わりに呼び出される。 3. typeは__new__メソッドでクラス生成の主な処理を行う。メタク ラスを使うとクラス定義時に処理が実行される。 4. サンプルでは、クラス生成の前にTestSequenseMeta()が呼び出さ れ、クラス定義が返る
  15. 15. nose × generator param_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 == b AssertionError ---------------------------------------------------------------------- Ran 3 tests in 0.002s FAILED (failures=1) nosetestsコマンドでtest_xx.pyなファイルを探し てtest_な関数を実行してくれる。
  16. 16. nose_parameterized from nose_parameterized import parameterized import 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の引数で テストの設定を渡す感じ?
  17. 17. 動的メソッド定義でWebTestしてみた import unittest from 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) ------------------------------------------------------- Ran 3 tests in 0.298s OK でした。
  18. 18. - メタプログラミングなのにアドホックすぎ! - さすがにこのままコレがしたいわけではない。 - 設定出来る項目が多すぎるのかも。 - テストのルールを整備しかつ大量なテストであれば使 えなくは無いかも。
  19. 19. 参考サイト How to generate dynamic (parametrized) unit tests in python? http://stackoverflow.com/questions/32899/how-to-generate-dynamic-parametrized-unit-tests-in-python メタプログラミングPython https://tell-k.github.io/pyconjp2016/#1 Pythonによる黒魔術入門 http://www.slideshare.net/ssuser38b704/ll-lang-blackmagic Pythonでメタプログラミング http://qiita.com/fujimisakari/items/0d4786dd9ddeed4eb702

×