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 mockservere 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-backende 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-backendmanipular 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-backendchama 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-backendchama 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.