web-dev-qa-db-ja.com

GoogleCloudエンドポイントを単体テストする方法

Google CloudEndpointsの単体テストを設定するためのサポートが必要です。 WebTestを使用すると、すべてのリクエストがAppErrorで応答します:不正な応答:404が見つかりません。エンドポイントがWebTestと互換性があるかどうかはよくわかりません。

これは、アプリケーションが生成される方法です。

application = endpoints.api_server([TestEndpoint], restricted=False)

次に、WebTestを次のように使用します。

client = webtest.TestApp(application)
client.post('/_ah/api/test/v1/test', params)

Curlを使用したテストは正常に機能します。

エンドポイントのテストを別のものに書く必要がありますか? GAEエンドポイントチームからの提案は何ですか?

38
Jairo Vasquez

多くの実験とSDKコードを調べた後、Python内でエンドポイントをテストする2つの方法を思いつきました。

1. webtest + testbedを使用してSPI側をテストする

あなたはwebtestで正しい方向に進んでいますが、SPIエンドポイントに対するリクエストを正しく変換することを確認する必要があります。

Cloud EndpointsAPIフロントエンドと_dev_appserver_のEndpointsDispatcherは、_/_ah/api/*_への呼び出しを_/_ah/spi/*_への対応する「バックエンド」呼び出しに変換します。変換は次のように思われます:

  • すべての呼び出しは_application/json_ HTTP POSTです(RESTエンドポイントが別のものであっても)。
  • リクエストパラメータ(パス、クエリ、JSON本文)はすべて1つのJSON本文メッセージにマージされます。
  • 「バックエンド」エンドポイントは、URLで実際のpythonクラス名とメソッド名を使用します。たとえば、_POST /_ah/spi/TestEndpoint.insert_message_はコードでTestEndpoint.insert_message()を呼び出します。
  • JSON応答は、元のクライアントに返される前にのみ再フォーマットされます。

これは、次の設定でエンドポイントをテストできることを意味します。

_from google.appengine.ext import testbed
import webtest
# ...
def setUp(self):
    tb = testbed.Testbed()
    tb.setup_env(current_version_id='testbed.version') #needed because endpoints expects a . in this value
    tb.activate()
    tb.init_all_stubs()
    self.testbed = tb

def tearDown(self):
    self.testbed.deactivate()

def test_endpoint_insert(self):
    app = endpoints.api_server([TestEndpoint], restricted=False)
    testapp = webtest.TestApp(app)
    msg = {...} # a dict representing the message object expected by insert
                # To be serialised to JSON by webtest
    resp = testapp.post_json('/_ah/spi/TestEndpoint.insert', msg)

    self.assertEqual(resp.json, {'expected': 'json response msg as dict'})
_

ここで重要なのは、エンドポイントを呼び出す前に、データストアまたは他のGAEサービスで適切なフィクスチャを簡単にセットアップできるため、呼び出しの予想される副作用をより完全に主張できることです。

2.完全統合テストのために開発サーバーを起動します

次のようなものを使用して、同じpython環境内で開発サーバーを起動できます。

_import sys
import os
import dev_appserver
sys.path[1:1] = dev_appserver._DEVAPPSERVER2_PATHS

from google.appengine.tools.devappserver2 import devappserver2
from google.appengine.tools.devappserver2 import python_runtime
# ...
def setUp(self):
    APP_CONFIGS = ['/path/to/app.yaml'] 
    python_runtime._RUNTIME_ARGS = [
        sys.executable,
        os.path.join(os.path.dirname(dev_appserver.__file__),
                     '_python_runtime.py')
    ]
    options = devappserver2.PARSER.parse_args([
        '--admin_port', '0',
        '--port', '8123', 
        '--datastore_path', ':memory:',
        '--logs_path', ':memory:',
        '--skip_sdk_update_check',
        '--',
    ] + APP_CONFIGS)
    server = devappserver2.DevelopmentServer()
    server.start(options)
    self.server = server

def tearDown(self):
    self.server.stop()
_

ここで、APIに対してテストを実行するためにlocalhost:8123にactualHTTPリクエストを発行する必要がありますが、GAEAPIと対話してフィクスチャなどを設定することもできます。 。テストを実行するたびに新しい開発サーバーを作成および破棄するため、これは明らかに低速です。

この時点で、HTTPリクエストを自分で作成する代わりに、 Google API Python client を使用してAPIを使用します。

_import apiclient.discovery
# ...
def test_something(self):
    apiurl = 'http://%s/_ah/api/discovery/v1/apis/{api}/{apiVersion}/rest' \
                    % self.server.module_to_address('default')
    service = apiclient.discovery.build('testendpoint', 'v1', apiurl)

    res = service.testresource().insert({... message ... }).execute()
    self.assertEquals(res, { ... expected reponse as dict ... })
_

これは、GAE APIに直接アクセスしてフィクスチャを簡単にセットアップし、内部状態を検査できるため、CURLを使用したテストよりも改善されています。エンドポイントディスパッチメカニズムを実装する開発サーバーの最小限のコンポーネントをつなぎ合わせることでHTTPをバイパスする統合テストを行うさらに良い方法があると思いますが、これには現在よりも多くの調査時間が必要です。

30
Ezequiel Muns

webtest名前のバグを減らすために簡略化できます

次の場合TestApi

import endpoints
import protorpc
import logging

class ResponseMessageClass(protorpc.messages.Message):
    message = protorpc.messages.StringField(1)
class RequestMessageClass(protorpc.messages.Message):
    message = protorpc.messages.StringField(1)


@endpoints.api(name='testApi',version='v1',
               description='Test API',
               allowed_client_ids=[endpoints.API_Explorer_CLIENT_ID])
class TestApi(protorpc.remote.Service):

    @endpoints.method(RequestMessageClass,
                      ResponseMessageClass,
                      name='test',
                      path='test',
                      http_method='POST')
    def test(self, request):
        logging.info(request.message)
        return ResponseMessageClass(message="response message")

tests.pyは次のようになります

import webtest
import logging
import unittest
from google.appengine.ext import testbed
from protorpc.remote import protojson
import endpoints

from api.test_api import TestApi, RequestMessageClass, ResponseMessageClass


class AppTest(unittest.TestCase):
    def setUp(self):
        logging.getLogger().setLevel(logging.DEBUG)

        tb = testbed.Testbed()
        tb.setup_env(current_version_id='testbed.version') 
        tb.activate()
        tb.init_all_stubs()
        self.testbed = tb


    def tearDown(self):
        self.testbed.deactivate()


    def test_endpoint_testApi(self):
        application = endpoints.api_server([TestApi], restricted=False)

        testapp = webtest.TestApp(application)

        req = RequestMessageClass(message="request message")

        response = testapp.post('/_ah/spi/' + TestApi.__name__ + '.' + TestApi.test.__name__, protojson.encode_message(req),content_type='application/json')

        res = protojson.decode_message(ResponseMessageClass,response.body)

        self.assertEqual(res.message, 'response message')


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

これらを通常の方法でテストできるように、考えられるすべてのことを試しました。/_ah/spiメソッドを直接ヒットするだけでなく、service_mappingsを使用して新しいprotorpcアプリを作成しようとしても役に立ちませんでした。私はエンドポイントチームのGoogle社員ではないので、これを機能させるための賢い方法があるかもしれませんが、Webtestを使用するだけでは機能しないようです(明らかな何かを見逃していない限り)。

それまでの間、分離された環境でApp Engineテストサーバーを起動し、httpリクエストを発行するテストスクリプトを作成できます。

分離された環境でサーバーを実行する例(bashですが、Pythonから簡単に実行できます):

DATA_PATH=/tmp/appengine_data

if [ ! -d "$DATA_PATH" ]; then
    mkdir -p $DATA_PATH
fi

dev_appserver.py --storage_path=$DATA_PATH/storage --blobstore_path=$DATA_PATH/blobstore --datastore_path=$DATA_PATH/datastore --search_indexes_path=$DATA_PATH/searchindexes --show_mail_body=yes --clear_search_indexes --clear_datastore .

次に、リクエストを使用してalacurlをテストできます。

requests.get('http://localhost:8080/_ah/...')
2

私のソリューションでは、テストモジュール全体に1つのdev_appserverインスタンスを使用します。これは、テストメソッドごとにdev_appserverを再起動するよりも高速です。

GoogleのPython APIクライアントライブラリを使用することで、APIと対話するための最も簡単で同時に最も強力な方法も得られます。

import unittest
import sys
import os

from apiclient.discovery import build
import dev_appserver


sys.path[1:1] = dev_appserver.EXTRA_PATHS

from google.appengine.tools.devappserver2 import devappserver2
from google.appengine.tools.devappserver2 import python_runtime

server = None


def setUpModule():
    # starting a dev_appserver instance for testing
    path_to_app_yaml = os.path.normpath('path_to_app_yaml')
    app_configs = [path_to_app_yaml]
    python_runtime._RUNTIME_ARGS = [
        sys.executable,
        os.path.join(os.path.dirname(dev_appserver.__file__),         
        '_python_runtime.py')
        ]
    options = devappserver2.PARSER.parse_args(['--port', '8080',
                                           '--datastore_path', ':memory:',
                                           '--logs_path', ':memory:',
                                           '--skip_sdk_update_check',
                                           '--',
                                           ] + app_configs)
    global server
    server = devappserver2.DevelopmentServer()
    server.start(options)


def tearDownModule():
    # shutting down dev_appserver instance after testing
    server.stop()


class MyTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        # build a service object for interacting with the api
        # dev_appserver must be running and listening on port 8080
        api_root = 'http://127.0.0.1:8080/_ah/api'
        api = 'my_api'
        version = 'v0.1'
        discovery_url = '%s/discovery/v1/apis/%s/%s/rest' % (api_root, api,                     
                                                             version)
        cls.service = build(api, version, discoveryServiceUrl=discovery_url)

    def setUp(self):
        # create a parent entity and store its key for each test run
        body = {'name': 'test  parent'}
        response = self.service.parent().post(body=body).execute()   
        self.parent_key = response['parent_key']

    def test_post(self):
        # test my post method 
        # the tested method also requires a path argument "parent_key" 
        # .../_ah/api/my_api/sub_api/post/{parent_key}
        body = {'SomeProjectEntity': {'SomeId': 'abcdefgh'}}
        parent_key = self.parent_key
        req = self.service.sub_api().post(body=body,parent_key=parent_key)
        response = req.execute()
        etc..
1
Arne Wolframm

Ezequiel Munsによって説明されているように完全なHTTPスタックをテストしたくない場合は、endpoints.methodをモックアウトして、API定義を直接テストすることもできます。

def null_decorator(*args, **kwargs):
    def decorator(method):
        def wrapper(*args, **kwargs):
            return method(*args, **kwargs)
        return wrapper
    return decorator

from google.appengine.api.users import User
import endpoints
endpoints.method = null_decorator
# decorator needs to be mocked out before you load you endpoint api definitions
from mymodule import api


class FooTest(unittest.TestCase):
    def setUp(self):
        self.api = api.FooService()

    def test_bar(self):
        # pass protorpc messages directly
        self.api.foo_bar(api.MyRequestMessage(some='field'))
1
schibum

ソースを掘り下げた後、2014年のEzequiel Munsの(優れた)回答以降、エンドポイントの状況が変わったと思います。方法1の場合、/ _ ah/api/*から直接リクエストし、/を使用する代わりに正しいHTTPメソッドを使用する必要があります。 _ah/spi/*変換。これにより、テストファイルは次のようになります。

from google.appengine.ext import testbed
import webtest
# ...
def setUp(self):
    tb = testbed.Testbed()
    # Setting current_version_id doesn't seem necessary anymore
    tb.activate()
    tb.init_all_stubs()
    self.testbed = tb

def tearDown(self):
    self.testbed.deactivate()

def test_endpoint_insert(self):
    app = endpoints.api_server([TestEndpoint]) # restricted is no longer required
    testapp = webtest.TestApp(app)
    msg = {...} # a dict representing the message object expected by insert
                # To be serialised to JSON by webtest
    resp = testapp.post_json('/_ah/api/test/v1/insert', msg)

    self.assertEqual(resp.json, {'expected': 'json response msg as dict'})

検索のために、古い方法を使用した場合の症状は、エンドポイントがValueErrorInvalid request path: /_ah/spi/whateverで発生させることです。それが誰かの時間を節約することを願っています!

1
Carter De Leo