Yandex abre o Testinguite



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:

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


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.

Nível de detalheFerramenta de teste
Método / Função, Classe, Componente, BibliotecaTestes de unidade padrão, pytest , Googletest , às vezes ainda testam
Microserviçosuíte de teste
Conjunto de microsserviço (app)Testes de integração do Testsuite (não abordados neste artigo)

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

#      . 
#       ( 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

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, # 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)

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.

Fonte

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

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.

Fonte

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


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.

Fonte

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')

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

--  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');

Fonte

#  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',
            },
        ],
    }

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



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

#   PostgreSQL
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.

All Articles