Как модульное тестирование конечных точек Google Cloud

мне нужна помощь в настройке unittests для конечных точек Google Cloud. Использование 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 команда?

6 ответов


после долгих экспериментов и просмотра кода SDK я придумал два способа тестирования конечных точек в python:

1. Использование webtest + testbed для тестирования стороны SPI

вы на правильном пути с webtest, но просто нужно убедиться, что вы правильно преобразовать ваши запросы для конечной точки SPI.

интерфейс API конечных точек облака и EndpointsDispatcher на dev_appserver преобразование звонки /_ah/api/* в соответствующий "бэкэнд" звонки /_ah/spi/*. Трансформация выглядит так:

  • все вызовы application/json http-сообщения (даже если конечная точка REST-это что-то другое).
  • параметры запроса (путь, запрос и тело JSON) объединяются в одно сообщение тела JSON.
  • конечная точка "backend" использует фактические имена классов и методов python в URL-адресе, например 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. Запуск сервера разработки для полного интеграционного тестирования

вы можете запустить dev-сервер в том же 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()

теперь вам нужно выпустить фактический HTTP-запросы к localhost: 8123 для запуска тестов против API, но снова может взаимодействовать с API GAE для настройки светильников и т. д. Это, очевидно, медленно, поскольку вы создаете и уничтожаете новый dev-сервер для каждого тестового запуска.

в этот момент я использую Google API Python client использовать API вместо построения HTTP-запросов сам:

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 ... })

это улучшение по сравнению с тестированием с CURL, поскольку это дает вам прямой доступ к API GAE, чтобы легко настроить светильники и проверить внутреннее состояние. Я подозреваю, что есть еще лучший способ сделать интеграционное тестирование, которое обходит HTTP, сшивая минимальные компоненты на dev-сервере, которые реализуют механизм отправки конечных точек, но это требует больше времени исследования, чем у меня сейчас.


web-сайтов можно упростить, чтобы уменьшить ошибки именования

следующее внесением ошибок на базе 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()

Я пробовал все, что мог придумать, чтобы их можно было протестировать обычным способом. Я попытался напрямую использовать методы /_ah/spi, а также даже создать новое приложение protorpc с помощью service_mappings безрезультатно. Я не Гуглер в команде конечных точек, поэтому, возможно, у них есть что-то умное, чтобы позволить этому работать, но не похоже, что простое использование 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 .

вы можете просто использовать запросы для тестирования ALA curl:

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

Если вы не хотите тестировать полный стек HTTP, как описано Ezequiel Muns, вы также можете просто издеваться над конечными точками.метод и проверьте определение 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'))

мое решение использует один экземпляр dev_appserver для всего тестового модуля, что быстрее, чем перезапуск dev_appserver для каждого метода тестирования.

используя клиентскую библиотеку API Python от Google, я также получаю самый простой и в то же время самый мощный способ взаимодействия с моим 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..

после копания в источниках, я считаю, что все изменилось в конечных точках с момента ответа Езекиля Мунса (отлично) в 2014 году. Для метода 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'})

для поиска, симптом использования старого метода-конечные точки, вызывающие ValueError С Invalid request path: /_ah/spi/whatever. Надеюсь, это сэкономит кому-то время!