Hoje, abrimos o código-fonte do testsuite, uma estrutura para testar serviços HTTP, desenvolvida e usada pelo Yandex.Taxi. As fontes são publicadas no GitHub sob a licença MIT.Com o testsuite, é conveniente testar serviços HTTP. Ele fornece mecanismos prontos para:O backend Yandex.Taxi consiste em centenas de microsserviços, novos estão aparecendo constantemente. Desenvolvemos todos os serviços altamente carregados em C ++ usando nossa própria estrutura userver, já conversamos sobre isso no Habré . Fazemos serviços e protótipos menos exigentes em Python.Para garantir que o serviço resolva bem o problema, fornecendo a API para outros serviços e o aplicativo final, queremos testá-lo como um todo, principalmente no princípio de uma caixa preta.Não há ferramentas prontas para isso - você precisaria escrever código para configurar um ambiente de teste, que seria:- levantar e preencher o banco de dados;
- interceptar e falsificar solicitações HTTP;
- execute um serviço de teste neste ambiente.
Resolver esse problema usando as estruturas para testes de unidade é muito difícil e errado, porque sua tarefa é diferente: teste de unidade de unidades estruturais menores - componentes, classes, funções.O Testsuite é baseado no pytest , a estrutura de teste padrão do Python. Não importa em que idioma está escrito o microsserviço em que estamos testando. Agora o testsuite é executado nos sistemas operacionais GNU / Linux, macOS.Embora o testsuite seja conveniente para cenários de integração, ou seja, a interação de vários serviços (e se o serviço for escrito em Python, então para os de baixo nível), não consideraremos esses casos. Além disso, focaremos apenas o teste de um único serviço.Princípio de funcionamento
O objetivo final é garantir que o serviço responda corretamente às chamadas HTTP, para que testemos através de chamadas HTTP.Iniciar / parar um serviço é uma operação de rotina. Portanto, verificamos:- que depois de iniciar o serviço responde via HTTP;
- como o serviço se comporta se serviços externos estiverem temporariamente indisponíveis.

Suíte de teste:- Inicia o banco de dados (PostgreSQL, MongoDB ...).
- Antes de cada teste, ele preenche o banco de dados com dados de teste.
- Inicia o microsserviço testado em um processo separado.
- Lança seu próprio servidor web (mockserver), que imita (seca) o ambiente externo do serviço.
- Executa testes.
Os testes podem verificar:- Se o serviço manipula solicitações HTTP corretamente.
- Como o serviço funciona diretamente no banco de dados.
- A presença / ausência / sequência de chamadas para serviços externos.
- O estado interno do serviço usando as informações que ele passa para o Testpoint.
mockserver
Testamos o comportamento de um único microsserviço. As chamadas para a API HTTP de serviços externos devem ser conectadas em ponte. Para essa parte do trabalho no testsuite, conheça seus próprios plug-ins mockserver
e mockserver_https
. O Mockserver é um servidor HTTP com manipuladores de solicitação personalizáveis para cada teste e memória sobre quais solicitações são processadas e quais dados são transferidos.Base de dados
O Testsuite permite que o teste acesse diretamente o banco de dados para leitura e gravação. Usando os dados, você pode formular uma pré-condição para o teste e verificar o resultado. Suporte imediato ao PostgreSQL, MongoDB, Redis.Como começar a usar
Para escrever testes testsuite, o desenvolvedor deve conhecer o Python e a estrutura padrão do pytest .Vamos demonstrar usando o testsuite passo a passo usando um exemplo simples de bate-papo. Aqui estão os códigos-fonte para o aplicativo e os testes.
O front- end chat.html interage com o serviço de back-end de bate-papo .Para demonstrar como os serviços interagem, o back-end delega o armazenamento de mensagens no serviço de repositório. O armazenamento é implementado de duas maneiras, chat-storage-mongo e chat-storage-postgres .back-end de bate-papo
O serviço de back-end de bate-papo é o ponto de entrada para solicitações do front-end. Capaz de enviar e retornar uma lista de mensagens.Serviço
Mostramos um exemplo de manipulador de solicitação POST /messages/retrieve
:Código-fonte@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)
Testes
Prepare a infraestrutura do testinguite para o lançamento do serviço. Indicamos com quais configurações queremos iniciar o serviço.Fonte
@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
Definimos o dispositivo cliente, através dele o teste envia uma solicitação HTTP para o serviço.Fonte@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)
Agora a infraestrutura sabe como iniciar chat-backend
e como enviar uma solicitação a ela. Isso é suficiente para começar a escrever testes.Observe que, nos testes chat-backend
, não usamos serviços de armazenamento, nem chat-storage-mongo
, nem chat-storage-postgres
. Para chat-backend
manipular chamadas normalmente, nós molhamos a API de armazenamento mockserver
.Vamos escrever um teste para o método POST messages/send
. Verificamos que:- a solicitação é processada normalmente;
- ao processar a solicitação,
chat-backend
chama o método de armazenamento POST messages/send
.
Fonteasync 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
Vamos escrever um teste para o método POST messages/retrieve
. Verificamos que:- a solicitação é processada normalmente;
- ao processar a solicitação,
chat-backend
chama o método de armazenamento POST /messages/retrieve
; chat-backend
"Vira" a lista de mensagens recebidas do repositório para que as mensagens mais recentes estejam no final da lista.
Fonteasync 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
chat-storage-postgres
O serviço chat-storage-postgres
é responsável pela leitura e gravação de mensagens de bate-papo no banco de dados PostgreSQL.Serviço
É assim que lemos a lista de mensagens do PostgreSQL no método POST /messages/retrieve
:Código-fonte@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})
Testes
O serviço que estamos testando usa o banco de dados PostgreSQL. Para que tudo funcione, tudo o que você precisa fazer é informar ao testsuite em qual diretório procurar esquemas de tabela.Fonte@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()))
O restante da configuração da infraestrutura conftest.py não é diferente do serviço descrito acima chat-backend
.Vamos passar para os testes.Vamos escrever um teste para o método POST messages/send
. Verifique se ele salva a mensagem no banco de dados.Fonteasync 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')
Vamos escrever um teste para o método POST messages/retrieve
. Verifique se ele retorna mensagens do banco de dados.Primeiro, crie um script que adicione os registros necessários à tabela. O Testsuite executará automaticamente o script antes do teste.Fonte
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');
Fonte
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',
},
],
}
Lançamento
A execução de exemplos é mais fácil em um contêiner de docker. Para fazer isso, você precisa do docker e docker-compose instalados na máquina.Todos os exemplos são iniciados no diretório Iniciar bate-papo .docs/examples
docs/examples$ make run-chat-mongo
docs/examples$ make run-chat-postgres
Após o lançamento, o URL será exibido no console, onde você pode abrir o bate-papo no navegador:chat-postgres_1 | ======== Running on http://0.0.0.0:8081 ========
chat-postgres_1 | (Press CTRL+C to quit)
Executar testes
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
Documentação
A documentação detalhada do testsuite está disponível aqui .Instruções para configurar e executar exemplos.Se você tiver perguntas github.com/yandex/yandex-taxi-testsuite/issues - deixe um comentário.