Yandex abre Testsuite



Hoy abrimos el código fuente de testsuite, un marco para probar servicios HTTP, que fue desarrollado y utilizado por Yandex.Taxi. Las fuentes se publican en GitHub bajo la licencia MIT.

Con testsuite, es conveniente probar los servicios HTTP. Proporciona mecanismos listos para:

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


El backend Yandex.Taxi consta de cientos de microservicios, constantemente aparecen nuevos. Desarrollamos todos los servicios altamente cargados en C ++ utilizando nuestro propio marco de usuario, ya lo hemos hablado en Habré . Realizamos servicios y prototipos menos exigentes en Python.

Para asegurarnos de que el servicio resuelva bien su problema, proporcionando la API a otros servicios y la aplicación final, queremos probarlo como un todo, principalmente en el principio de una caja negra.

No hay herramientas listas para esto; tendría que escribir código para configurar un entorno de prueba, que sería:

  • levantar y llenar la base de datos;
  • interceptar y falsificar solicitudes HTTP;
  • ejecutar un servicio de prueba en este entorno.

Resolver este problema usando los marcos para las pruebas unitarias es demasiado difícil e incorrecto, porque su tarea es diferente: pruebas unitarias de unidades estructurales más pequeñas: componentes, clases, funciones.

Testsuite se basa en pytest , el marco de prueba estándar de Python. No importa en qué idioma esté escrito el microservicio que estamos probando. Ahora testsuite se ejecuta en GNU / Linux, sistemas operativos macOS.

A pesar de que la suite de prueba es conveniente para escenarios de integración, es decir, la interacción de varios servicios (y si el servicio está escrito en Python, entonces para los de bajo nivel), no consideraremos estos casos. Además, nos centraremos solo en probar un solo servicio.

Nivel de detalleHerramienta de prueba
Método / Función, Clase, Componente, BibliotecaPruebas unitarias estándar, pytest , Googletest , a veces todavía testuite
MicroservicioBanco de pruebas
Conjunto de microservicios (aplicación)Pruebas de integración de Testsuite (no cubiertas en este artículo)

Principio de operación


El objetivo final es asegurarse de que el servicio responda las llamadas HTTP correctamente, por lo que probamos las llamadas HTTP.

Iniciar / detener un servicio es una operación de rutina. Por lo tanto, verificamos:

  • que después de iniciar el servicio responde a través de HTTP;
  • cómo se comporta el servicio si los servicios externos no están disponibles temporalmente.




Banco de pruebas:

  • Inicia la base de datos (PostgreSQL, MongoDB ...).
  • Antes de cada prueba, llena la base de datos con datos de prueba.
  • Inicia el microservicio probado en un proceso separado.
  • Lanza su propio servidor web (mockserver), que imita (seca) el entorno externo para el servicio.
  • Realiza pruebas.

Las pruebas pueden verificar:

  • Si el servicio maneja las solicitudes HTTP correctamente.
  • Cómo funciona el servicio directamente en la base de datos.
  • La presencia / ausencia / secuencia de llamadas a servicios externos.
  • El estado interno del servicio que utiliza la información que pasa a Testpoint.

mockserver


Probamos el comportamiento de un solo microservicio. Las llamadas a la API HTTP de servicios externos deben puentearse. Para esta parte del trabajo en el testuite cumplir con sus propios complementos mockservery mockserver_https. Mockserver es un servidor HTTP con procesadores de solicitudes que se pueden personalizar para cada prueba y memoria sobre qué solicitudes se procesan y qué datos se transfieren.

Base de datos


Testsuite permite que la prueba acceda directamente a la base de datos para leer y escribir. Usando los datos, puede formular una condición previa para la prueba y verificar el resultado. Fuera de la caja compatible con PostgreSQL, MongoDB, Redis.

Cómo comenzar a usar


Para escribir pruebas de testuite, un desarrollador debe conocer Python y el marco estándar de pytest .

Demostremos el uso de testsuite paso a paso con un simple ejemplo de chat. Aquí están los códigos fuente para la aplicación y las pruebas.



El frontend chat.html interactúa con el servicio backend de chat .

Para demostrar cómo interactúan los servicios, el backend de chat delega el almacenamiento de mensajes en el servicio de repositorio. El almacenamiento se implementa de dos maneras, chat-storage-mongo y chat-storage-postgres .

backend de chat


El servicio back-end de chat es el punto de entrada para las solicitudes del front-end. Capaz de enviar y devolver una lista de mensajes.

Servicio


Mostramos un ejemplo de manejador de solicitudes POST /messages/retrieve:

Código fuente

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

Pruebas


Prepare la infraestructura de prueba para el lanzamiento del servicio. Indicamos con qué configuración queremos iniciar el servicio.

Fuente

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

Configuramos el dispositivo del cliente, a través de él la prueba envía una solicitud HTTP al servicio.

Fuente

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

Ahora la infraestructura sabe cómo comenzar chat-backendy cómo enviarle una solicitud. Esto es suficiente para comenzar a escribir pruebas.

Tenga en cuenta que en las pruebas chat-backendno utilizamos servicios de almacenamiento, ni chat-storage-mongotampoco chat-storage-postgres. Para chat-backendmanejar las llamadas normalmente, humedecemos la API de almacenamiento con mockserver.

Escribamos una prueba para el método POST messages/send. Verificamos que:

  • la solicitud se procesa normalmente;
  • al procesar la solicitud, chat-backendllama al método de almacenamiento POST messages/send.

Fuente

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

Escribamos una prueba para el método POST messages/retrieve. Verificamos que:

  • la solicitud se procesa normalmente;
  • cuando procesa la solicitud, chat-backendllama al método de almacenamiento POST /messages/retrieve;
  • chat-backend "Voltea" la lista de mensajes recibidos del repositorio para que los últimos mensajes estén al final de la lista.

Fuente

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-almacenamiento-postgres



El servicio chat-storage-postgreses responsable de leer y escribir mensajes de chat en la base de datos PostgreSQL.

Servicio


Así es como leemos la lista de mensajes de PostgreSQL en el método POST /messages/retrieve:

Código fuente

@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})

Pruebas


El servicio que estamos probando usa la base de datos PostgreSQL. Para que todo funcione, todo lo que necesita hacer es decirle a testsuite en qué directorio buscar esquemas de tabla.

Fuente

@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()))

El resto de la configuración de la infraestructura conftest.py no es diferente del servicio descrito anteriormente chat-backend.

Pasemos a las pruebas.

Escribamos una prueba para el método POST messages/send. Compruebe que guarda el mensaje en la base de datos.

Fuente

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

Escribamos una prueba para el método POST messages/retrieve. Compruebe que devuelve mensajes de la base de datos.

Primero, cree un script que agregue los registros que necesitamos a la tabla. Testsuite ejecutará automáticamente el script antes de la prueba.

Fuente

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

Fuente

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

Lanzamiento


Ejecutar ejemplos es más fácil en un contenedor acoplable. Para hacer esto, necesita la ventana acoplable y docker-compose instalada en la máquina.

Todos los ejemplos se inician desde el directorio Iniciar chat .docs/examples



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

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

Después del lanzamiento, la URL se mostrará en la consola, donde puede abrir el chat en el navegador:

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

Ejecutar pruebas

#    
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

Documentación


La documentación detallada del testuite está disponible aquí .

Instrucciones para configurar y ejecutar ejemplos.

Si tiene preguntas sobre github.com/yandex/yandex-taxi-testsuite/issues , deje un comentario.

All Articles