Yandex ouvre Testsuite



Aujourd'hui, nous ouvrons le code source de testsuite, un framework pour tester les services HTTP, qui a été développé et utilisé par Yandex.Taxi. Les sources sont publiées sur GitHub sous la licence MIT.

Avec testsuite, il est pratique de tester les services HTTP. Il fournit des mécanismes prêts à l'emploi pour:

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


Le backend Yandex.Taxi se compose de centaines de microservices, de nouveaux apparaissent constamment. Nous développons tous les services très chargés en C ++ en utilisant notre propre framework userver, nous en avons déjà parlé sur Habré . Nous réalisons des services et des prototypes moins exigeants en Python.

Pour nous assurer que le service résout bien son problème, en fournissant l'API à d'autres services et à l'application finale, nous voulons le tester dans son ensemble, principalement sur le principe d'une boîte noire.

Il n'y a pas d'outils prĂŞts Ă  l'emploi pour cela - vous devrez Ă©crire du code pour configurer un environnement de test, qui serait:

  • augmenter et remplir la base de donnĂ©es;
  • intercepter et usurper les requĂŞtes HTTP;
  • exĂ©cuter un service de test dans cet environnement.

Résoudre ce problème en utilisant les frameworks pour les tests unitaires est trop difficile et erroné, car leur tâche est différente: tests unitaires de petites unités structurelles - composants, classes, fonctions.

Testsuite est basé sur pytest , le framework de test Python standard. Peu importe la langue dans laquelle le microservice que nous testons est écrit. Maintenant, la suite de tests fonctionne sur GNU / Linux, les systèmes d'exploitation macOS.

Bien que la suite de tests soit pratique pour les scénarios d'intégration, c'est-à-dire l'interaction de plusieurs services (et si le service est écrit en Python, alors pour les services de bas niveau), nous ne considérerons pas ces cas. De plus, nous nous concentrerons uniquement sur le test d'un seul service.

Niveau de détailOutil de test
Méthode / fonction, classe, composant, bibliothèqueTests unitaires standard, pytest , Googletest , parfois encore suite de tests
Microservicesuite de tests
Ensemble de microservices (application)Tests d'intégration de Testsuite (non traités dans cet article)

Principe de fonctionnement


Le but ultime est de s'assurer que le service répond correctement aux appels HTTP, nous testons donc les appels HTTP.

Le démarrage / l'arrêt d'un service est une opération de routine. Par conséquent, nous vérifions:

  • qu'après le dĂ©marrage du service rĂ©pond via HTTP;
  • le comportement du service si les services externes sont temporairement indisponibles.




Suite de tests:

  • DĂ©marre la base de donnĂ©es (PostgreSQL, MongoDB ...).
  • Avant chaque test, il remplit la base de donnĂ©es avec des donnĂ©es de test.
  • DĂ©marre le microservice testĂ© dans un processus distinct.
  • Lance son propre serveur Web (mockserver), qui imite (assèche) l'environnement du service.
  • Effectue des tests.

Les tests peuvent vérifier:

  • Indique si le service gère correctement les requĂŞtes HTTP.
  • Fonctionnement du service directement dans la base de donnĂ©es.
  • La prĂ©sence / absence / sĂ©quence d'appels vers des services externes.
  • L'Ă©tat interne du service utilisant les informations qu'il transmet Ă  Testpoint.

mockserver


Nous testons le comportement d'un seul microservice. Les appels à l'API HTTP des services externes doivent être pontés. Pour cette partie du travail dans la suite de tests rencontrer ses propres plug-ins mockserveret mockserver_https. Mockserver est un serveur HTTP avec des gestionnaires de demandes personnalisables pour chaque test et mémoire sur les demandes qui sont traitées et les données qui sont transférées.

Base de données


Testsuite permet au test d'accéder directement à la base de données pour la lecture et l'écriture. À l'aide des données, vous pouvez formuler une condition préalable au test et vérifier le résultat. Prêt à l'emploi, PostgreSQL, MongoDB, Redis.

Comment commencer Ă  utiliser


Pour écrire des tests testsuite, un développeur doit connaître Python et le framework standard pytest .

DĂ©montrons l'utilisation Ă©tape par Ă©tape de la suite de tests Ă  l'aide d'un simple exemple de discussion Voici les codes sources de l'application et des tests.



Le frontend chat.html interagit avec le service de chat-backend .

Pour montrer comment les services interagissent, le back-end délègue le stockage des messages au service de référentiel. Le stockage est implémenté de deux manières, chat-storage-mongo et chat-storage-postgres .

backend de chat


Le service de chat back-end est le point d'entrée pour les demandes du front-end. Capable d' envoyer et de renvoyer une liste de messages.

Un service


Nous montrons un exemple de gestionnaire de requĂŞtes POST /messages/retrieve:

Code source

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

Les tests


Préparez l'infrastructure de la suite de tests pour le lancement du service. Nous indiquons avec quels paramètres nous voulons démarrer le service.

La source

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

Nous définissons le dispositif client, à travers lui, le test envoie une demande HTTP au service.

La source

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

L'infrastructure sait maintenant comment démarrer chat-backendet comment lui envoyer une demande. Cela suffit pour commencer à écrire des tests.

Veuillez noter que dans les tests, chat-backendnous n'utilisons pas les services de stockage, ni chat-storage-mongo, ni chat-storage-postgres. Pour chat-backendgérer les appels normalement, nous mouillons l'API de stockage avec mockserver.

Écrivons un test pour la méthode POST messages/send. Nous vérifions que:

  • la demande est traitĂ©e normalement;
  • lors du traitement de la demande, chat-backendappelle la mĂ©thode de stockage POST messages/send.

La source

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

Écrivons un test pour la méthode POST messages/retrieve. Nous vérifions que:

  • la demande est traitĂ©e normalement;
  • lors du traitement de la demande, chat-backendappelle la mĂ©thode de stockage POST /messages/retrieve;
  • chat-backend «Retourne» la liste des messages reçus du rĂ©fĂ©rentiel afin que les derniers messages soient Ă  la fin de la liste.

La source

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



Le service chat-storage-postgresest responsable de la lecture et de l'écriture des messages de discussion dans la base de données PostgreSQL.

Un service


Voici comment nous lisons la liste des messages de PostgreSQL dans la méthode POST /messages/retrieve:

Code source

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

Les tests


Le service que nous testons utilise la base de données PostgreSQL. Pour que tout fonctionne, il vous suffit d'indiquer à la suite de tests dans quel répertoire rechercher les schémas de table.

La source

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

Le reste de la configuration de l' infrastructure conftest.py n'est pas différent du service décrit ci-dessus chat-backend.

Passons aux tests.

Écrivons un test pour la méthode POST messages/send. Vérifiez qu'il enregistre le message dans la base de données.

La source

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

Écrivons un test pour la méthode POST messages/retrieve. Vérifiez qu'il renvoie des messages de la base de données.

Tout d'abord, créez un script qui ajoute les enregistrements dont nous avons besoin à la table. Testsuite exécutera automatiquement le script avant le test.

La source

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

La source

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

lancement


L'exécution d'exemples est plus simple dans un conteneur Docker. Pour ce faire, vous devez installer docker et docker-compose sur la machine.

Tous les exemples sont lancés à partir du répertoire Start chat .docs/examples



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

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

Après le lancement, l'URL sera affichée dans la console, où vous pourrez ouvrir le chat dans le navigateur:

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

Exécuter des tests

#    
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

Documentation


Une documentation détaillée de la suite de tests est disponible ici .

Instructions pour configurer et exécuter des exemples.

Si vous avez des questions github.com/yandex/yandex-taxi-testsuite/issues - laissez un commentaire.

All Articles