今天,我们开放测试套件的源代码,该套件是用于测试HTTP服务的框架,由Yandex.Taxi开发和使用。来源根据MIT许可在GitHub上发布。使用测试套件,可以方便地测试HTTP服务。它提供了现成的机制来:Yandex.Taxi后端由数百个微服务组成,新的微服务不断出现。我们使用自己的userver框架在C ++中开发所有高负载的服务,我们已经在Habré中进行过讨论。我们用Python提供要求较低的服务和原型。为了确保该服务能够很好地解决其问题,并向其他服务和最终应用程序提供API,我们希望将其作为一个整体进行测试,主要是基于黑匣子原理。尚无现成的工具-您将必须编写代码来设置测试环境,这将是:- 提出并填写数据库;
- 拦截和欺骗HTTP请求;
- 在此环境中运行测试服务。
使用框架进行单元测试来解决此问题太困难和错误,因为它们的任务是不同的:较小结构单元的单元测试-组件,类,功能。Testsuite基于标准的Python测试框架pytest。所测试的微服务使用哪种语言都无关紧要。现在,测试套件可以在GNU / Linux,macOS操作系统上运行。尽管testsuite对于集成方案很方便,即几个服务的交互(如果该服务是用Python编写的,那么对于低级的服务),我们将不考虑这些情况。此外,我们将仅专注于测试单个服务。工作原理
最终目标是确保服务正确回答HTTP调用,因此我们通过HTTP调用进行测试。启动/停止服务是一项常规操作。因此,我们检查:- 启动服务后通过HTTP响应;
- 如果外部服务暂时不可用,服务的行为方式。

测试套件:- 启动数据库(PostgreSQL,MongoDB ...)。
- 在每次测试之前,它将用测试数据填充数据库。
- 在单独的过程中启动经过测试的微服务。
- 启动自己的Web服务器(模拟服务器),该服务器模仿(干燥)该服务的外部环境。
- 执行测试。
测试可以检查:- 服务是否正确处理HTTP请求。
- 服务如何直接在数据库中工作。
- 对外部服务的呼叫的存在/不存在/顺序。
- 使用服务传递给测试点的信息的内部状态。
模拟服务器
我们测试单个微服务的行为。对外部服务的HTTP API的调用应桥接。对于测试套件中的这部分工作,需要使用自己的插件mockserver
和mockserver_https
。Mockserver是具有请求处理器的HTTP服务器,该请求处理器可针对每个测试和内存进行自定义,以了解有关处理哪些请求以及传输哪些数据的信息。数据库
Testsuite允许测试直接访问数据库以进行读写。使用数据,您可以为测试制定前提条件并检查结果。开箱即用的支持PostgreSQL,MongoDB,Redis。如何开始使用
要编写测试套件测试,开发人员必须了解Python和标准pytest框架。让我们通过一个简单的聊天示例逐步演示使用testsuite。这是应用程序和测试的源代码。
前端chat.html与chat-backend服务进行交互。为了演示服务如何交互,聊天后端将消息存储委托给存储库服务。存储以两种方式实现:chat-storage-mongo和chat-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)
测验
准备测试套件基础结构以启动服务。我们用什么设置指示我们要启动服务。资源
@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_info.base_url + 'storage/',
],
check_url=SERVICE_BASEURL + 'ping',
),
) as scope:
yield scope
我们设置了客户端固定装置,通过它测试将HTTP请求发送到服务。资源@pytest.fixture
async def server_client(
service_daemon,
service_client_options,
ensure_daemon_started,
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.handler('/storage/messages/send')
async def handle_send(request):
assert request.json == {
'username': 'Bob',
'text': 'Hello, my name is Bob!',
}
return mockserver.make_response(status=204)
response = await server_client.post(
'messages/send',
json={'username': 'Bob', 'text': 'Hello, my name is Bob!'},
)
assert response.status == 204
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.json_handler('/storage/messages/retrieve')
async def handle_retrieve(request):
return {'messages': messages}
response = await server_client.post('messages/retrieve')
assert response.status == 200
body = response.json()
assert body == {'messages': list(reversed(messages))}
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):
response = await server_client.post(
'/messages/send', json={'username': 'foo', 'text': 'bar'},
)
assert response.status_code == 200
data = response.json()
assert 'id' in data
cursor = pgsql['chat_messages'].cursor()
cursor.execute(
'SELECT username, text FROM messages WHERE id = %s', (data['id'],),
)
record = cursor.fetchone()
assert record == ('foo', 'bar')
让我们为该方法编写一个测试POST messages/retrieve
。检查它是否从数据库返回消息。首先,创建一个脚本,将我们需要的记录添加到表中。Testsuite将在测试前自动执行脚本。资源
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');
资源
async def test_messages_retrieve(server_client, pgsql):
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
docs/examples$ make run-chat-mongo
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-发表评论。