Yandex开设Testsuite



今天,我们开放测试套件的源代码,该套件是用于测试HTTP服务的框架,由Yandex.Taxi开发和使用。来源根据MIT许可GitHub上发布。

使用测试套件,可以方便地测试HTTP服务。它提供了现成的机制来:

  • HTTP API.
  • HTTP-, .
  • , .
  • , .


Yandex.Taxi后端由数百个微服务组成,新的微服务不断出现。我们使用自己的userver框架在C ++中开发所有高负载的服务,我们已经在Habré中进行过讨论我们用Python提供要求较低的服务和原型。

为了确保该服务能够很好地解决其问题,并向其他服务和最终应用程序提供API,我们希望将其作为一个整体进行测试,主要是基于黑匣子原理。

尚无现成的工具-您将必须编写代码来设置测试环境,这将是:

  • 提出并填写数据库;
  • 拦截和欺骗HTTP请求;
  • 在此环境中运行测试服务。

使用框架进行单元测试来解决此问题太困难和错误,因为它们的任务是不同的:较​​小结构单元的单元测试-组件,类,功能。Testsuite

基于标准的Python测试框架pytest所测试的微服务使用哪种语言都无关紧要。现在,测试套件可以在GNU / Linux,macOS操作系统上运行。

尽管testsuite对于集成方案很方便,即几个服务的交互(如果该服务是用Python编写的,那么对于低级的服务),我们将不考虑这些情况。此外,我们将仅专注于测试单个服务。

详细程度测试工具
方法/功能,类,组件,库标准单元测试pytestGoogletest,有时仍然是testsuite
微服务测试套件
微服务集成(app)Testsuite集成测试(本文未涵盖)

工作原理


最终目标是确保服务正确回答HTTP调用,因此我们通过HTTP调用进行测试。

启动/停止服务是一项常规操作。因此,我们检查:

  • 启动服务后通过HTTP响应;
  • 如果外部服务暂时不可用,服务的行为方式。




测试套件:

  • 启动数据库(PostgreSQL,MongoDB ...)。
  • 在每次测试之前,它将用测试数据填充数据库。
  • 在单独的过程中启动经过测试的微服务。
  • 启动自己的Web服务器(模拟服务器),该服务器模仿(干燥)该服务的外部环境。
  • 执行测试。

测试可以检查:

  • 服务是否正确处理HTTP请求。
  • 服务如何直接在数据库中工作。
  • 对外部服务的呼叫的存在/不存在/顺序。
  • 使用服务传递给测试点的信息的内部状态。

模拟服务器


我们测试单个微服务的行为。对外部服务的HTTP API的调用应桥接。对于测试套件中的这部分工作,需要使用自己的插件mockservermockserver_httpsMockserver是具有请求处理器的HTTP服务器,该请求处理器可针对每个测试和内存进行自定义,以了解有关处理哪些请求以及传输哪些数据的信息。

数据库


Testsuite允许测试直接访问数据库以进行读写。使用数据,您可以为测试制定前提条件并检查结果。开箱即用的支持PostgreSQL,MongoDB,Redis。

如何开始使用


要编写测试套件测试,开发人员必须了解Python和标准pytest框架

让我们通过一个简单的聊天示例逐步演示使用testsuite。这是应用程序和测试源代码。



前端chat.htmlchat-backend服务进行交互

为了演示服务如何交互,聊天后端将消息存储委托给存储库服务。存储以两种方式实现:chat-storage-mongochat-storage-postgres

聊天后端


聊天后端服务是前端请求的入口。能够发送返回消息列表。

服务


我们展示了一个示例请求处理程序POST /messages/retrieve

源代码

@routes.post('/messages/retrieve')
async def handle_list(request):
async with aiohttp.ClientSession() as session:
    #     
    response = await session.post(
        storage_service_url + 'messages/retrieve',
            timeout=HTTP_TIMEOUT,
        )
        response.raise_for_status()
        response_body = await response.json()

        #    ,      
        messages = list(reversed(response_body['messages']))
        result = {'messages': messages}
        return web.json_response(result)

测验


准备测试套件基础结构以启动服务。我们用什么设置指示我们要启动服务。

资源

#      . 
#       ( scope='session'),   
@pytest.fixture(scope='session')
async def service_daemon(
        register_daemon_scope, service_spawner, mockserver_info,
):
    python_path = os.getenv('PYTHON3', 'python3')
    service_path = pathlib.Path(__file__).parent.parent
    async with register_daemon_scope(
            name='chat-backend',
            spawn=service_spawner(
                #   .    —  ,
                #    
                [
                    python_path,
                    str(service_path.joinpath('server.py')),
                    '--storage-service-url',
                    #       mockserver,
                    #         mockserver   /storage
                    mockserver_info.base_url + 'storage/',
                ],
                #  URL,      
                check_url=SERVICE_BASEURL + 'ping',
            ),
    ) as scope:
        yield scope

我们设置了客户端固定装置,通过它测试将HTTP请求发送到服务。

资源

@pytest.fixture
async def server_client(
        service_daemon, # HTTP-  == 204
        service_client_options,
        ensure_daemon_started,
        #   mockserver ,      ,
        #    ,    
        mockserver,
):
    await ensure_daemon_started(service_daemon)
    yield service_client.Client(SERVICE_BASEURL, **service_client_options)

现在,基础架构知道了如何启动chat-backend以及如何向其发送请求。这足以开始编写测试。

请注意,在测试中,chat-backend我们既不使用存储服务chat-storage-mongo,也不使用chat-storage-postgres为了chat-backend正常处理调用,我们使用来润湿存储API mockserver

让我们为该方法编写一个测试POST messages/send我们验证:

  • 该请求已正常处理;
  • 处理请求时,chat-backend调用存储方法POST messages/send

资源

async def test_messages_send(server_client, mockserver):
    #    mockserver   POST messages/send
    @mockserver.handler('/storage/messages/send')    
    async def handle_send(request):
        # ,       ,
        #     chat-backend
        assert request.json == {
            'username': 'Bob',
            'text': 'Hello, my name is Bob!',
        }
        return mockserver.make_response(status=204)

    #    chat-backend
    response = await server_client.post(
        'messages/send',
        json={'username': 'Bob', 'text': 'Hello, my name is Bob!'},
    )
    
    # ,        HTTP-
    assert response.status == 204

    # ,  chat-backend       POST messages/send
    assert handle_send.times_called == 1

让我们为该方法编写一个测试POST messages/retrieve我们验证:

  • 该请求已正常处理;
  • 处理请求时,chat-backend调用存储方法POST /messages/retrieve
  • chat-backend “翻转”从存储库收到的消息列表,以便最新消息位于列表的末尾。

资源

async def test_messages_retrieve(server_client, mockserver):
    messages = [
        {
            'username': 'Bob',
            'created': '2020-01-01T12:01:00.000',
            'text': 'Hi, my name is Bob!',
        },
        {
            'username': 'Alice',
            'created': {'$date': '2020-01-01T12:02:00.000'},
            'text': 'Hi Bob!',
        },
    ]

    #    mockserver   POST messages/retrieve
    @mockserver.json_handler('/storage/messages/retrieve')
    async def handle_retrieve(request):
        return {'messages': messages}

    #    chat-backend
    response = await server_client.post('messages/retrieve')

    # ,        HTTP-
    assert response.status == 200

    body = response.json()
    
    # ,    chat-backend    ,
    #   ,       
    assert body == {'messages': list(reversed(messages))}

    # ,  chat-backend       POST messages/retrieve
    assert handle_retrieve.times_called == 1


聊天存储postgres



该服务chat-storage-postgres负责将聊天消息读取和写入PostgreSQL数据库。

服务


这就是我们从PostgreSQL方法中读取消息列表的方法POST /messages/retrieve

源代码

@routes.post('/messages/retrieve')
    async def get(request):
        async with app['pool'].acquire() as connection:
            records = await connection.fetch(
                'SELECT created, username, "text" FROM messages '
                'ORDER BY created DESC LIMIT 20',
            )
        messages = [
            {
                'created': record[0].isoformat(),
                'username': record[1],
                'text': record[2],
            }
            for record in records
        ]
        return web.json_response({'messages': messages})

测验


我们正在测试的服务使用PostgreSQL数据库。对于所有工作,您要做的就是告诉testsuite在哪个目录中查找表架构。

资源

@pytest.fixture(scope='session')
def pgsql_local(pgsql_local_create):
    # ,     
    tests_dir = pathlib.Path(__file__).parent
    sqldata_path = tests_dir.joinpath('../schemas/postgresql')
    databases = discover.find_databases('chat_storage_postgres', sqldata_path)
    return pgsql_local_create(list(databases.values()))

其余的conftest.py基础结构设置与上述服务没有什么不同chat-backend

让我们继续进行测试。

让我们为该方法编写一个测试POST messages/send检查是否将消息保存到数据库。

资源

async def test_messages_send(server_client, pgsql):
    #   POST /messages/send
    response = await server_client.post(
        '/messages/send', json={'username': 'foo', 'text': 'bar'},
    )

    # ,    
    assert response.status_code == 200

    # ,     JSON    
    data = response.json()
    assert 'id' in data

    #     PostgreSQL  
    cursor = pgsql['chat_messages'].cursor()
    cursor.execute(
        'SELECT username, text FROM messages WHERE id = %s', (data['id'],),
    )
    record = cursor.fetchone()

    # ,          , 
    #     HTTP-
    assert record == ('foo', 'bar')

让我们为该方法编写一个测试POST messages/retrieve检查它是否从数据库返回消息。

首先,创建一个脚本,将我们需要的记录添加到表中。Testsuite将在测试前自动执行脚本。

资源

--  chat-storage-postgres/tests/static/test_service/pg_chat_messages.sql
INSERT INTO messages(id, created, username, text) VALUES (DEFAULT, '2020-01-01 00:00:00.0+03', 'foo', 'hello, world!');
INSERT INTO messages(id, created, username, text) VALUES (DEFAULT, '2020-01-01 00:00:01.0+03', 'bar', 'happy ny');

资源

#  chat-storage-postgres/tests/test_service.py
async def test_messages_retrieve(server_client, pgsql):
    #     testsuite     
    #  pg_chat_messages.sql
    response = await server_client.post('/messages/retrieve', json={})
    assert response.json() == {
        'messages': [
            {
                'created': '2019-12-31T21:00:01+00:00',
                'text': 'happy ny',
                'username': 'bar',
            },
            {
                'created': '2019-12-31T21:00:00+00:00',
                'text': 'hello, world!',
                'username': 'foo',
            },
        ],
    }

发射


在Docker容器中运行示例是最简单的。为此,您需要在计算机上安装docker和docker-compose。

所有示例均从“ 开始聊天”目录中启动docs/examples



#   MongoDB
docs/examples$ make run-chat-mongo

#   PostgreSQL
docs/examples$ make run-chat-postgres

启动后,URL将显示在控制台中,您可以在其中在浏览器中打开聊天:

chat-postgres_1 | ======== Running on http://0.0.0.0:8081 ========
chat-postgres_1 | (Press CTRL+C to quit)

运行测试

#    
docs/examples$ make docker-runtests

#    
docs/examples$ make docker-runtests-mockserver-example
docs/examples$ make docker-runtests-mongo-example
docs/examples$ make docker-runtests-postgres-example

文献资料


详细的测试套件文档可在此处获得

说明建立和运行的例子。

如果您有问题github.com/yandex/yandex-taxi-testsuite/issues-发表评论。

All Articles