Yandex öffnet die Testsuite



Heute öffnen wir den Quellcode für testsuite, ein Framework zum Testen von HTTP-Diensten, das von Yandex.Taxi entwickelt und verwendet wurde. Quellen werden auf GitHub unter der MIT-Lizenz veröffentlicht.

Mit testsuite ist es bequem, HTTP-Dienste zu testen. Es bietet vorgefertigte Mechanismen für:

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


Das Yandex.Taxi-Backend besteht aus Hunderten von Microservices, ständig erscheinen neue. Wir entwickeln alle hoch geladenen Dienste in C ++ mit unserem eigenen Userver-Framework, darüber haben wir bereits in Habré gesprochen . In Python stellen wir weniger anspruchsvolle Services und Prototypen her.

Um sicherzustellen, dass der Dienst sein Problem gut löst und die API für andere Dienste und die endgültige Anwendung bereitstellt, möchten wir sie als Ganzes testen, hauptsächlich nach dem Prinzip einer Black Box.

Hierfür gibt es keine vorgefertigten Tools. Sie müssten Code schreiben, um eine Testumgebung einzurichten. Dies wäre:

  • Erhöhen und füllen Sie die Datenbank.
  • HTTP-Anfragen abfangen und fälschen;
  • Führen Sie in dieser Umgebung einen Testdienst aus.

Die Lösung dieses Problems mithilfe der Frameworks für Komponententests ist zu schwierig und falsch, da ihre Aufgabe unterschiedlich ist: Komponententests kleinerer struktureller Einheiten - Komponenten, Klassen, Funktionen.

Testsuite basiert auf pytest , dem Standard-Python- Testframework . Es spielt keine Rolle, in welcher Sprache der von uns getestete Microservice geschrieben ist. Jetzt läuft Testsuite unter GNU / Linux, MacOS-Betriebssystemen.

Obwohl die Testsuite für Integrationsszenarien geeignet ist, dh für die Interaktion mehrerer Dienste (und wenn der Dienst in Python geschrieben ist, für Dienste auf niedriger Ebene), werden diese Fälle nicht berücksichtigt. Außerdem konzentrieren wir uns nur auf das Testen eines einzelnen Dienstes.

DetaillierungsgradTestwerkzeug
Methode / Funktion, Klasse, Komponente, BibliothekStandard-Unit-Tests, Pytest , Googletest , manchmal noch Testsuite
MicroserviceTestsuite
Microservice Ensemble (App)Testsuite-Integrationstests (in diesem Artikel nicht behandelt)

Funktionsprinzip


Das ultimative Ziel ist es, sicherzustellen, dass der Dienst HTTP-Aufrufe korrekt beantwortet, sodass wir Tests über HTTP-Aufrufe durchführen.

Das Starten / Stoppen eines Dienstes ist eine Routineoperation. Deshalb prüfen wir:

  • dass der Dienst nach dem Start über HTTP antwortet;
  • Verhalten des Dienstes, wenn externe Dienste vorübergehend nicht verfügbar sind.




Testsuite:

  • Startet die Datenbank (PostgreSQL, MongoDB ...).
  • Vor jedem Test wird die Datenbank mit Testdaten gefüllt.
  • Startet den getesteten Microservice in einem separaten Prozess.
  • Startet einen eigenen Webserver (Mockserver), der die externe Umgebung für den Dienst imitiert (trocknet).
  • Führt Tests durch.

Tests können überprüfen:

  • Gibt an, ob der Dienst HTTP-Anforderungen korrekt verarbeitet.
  • Wie der Dienst direkt in der Datenbank funktioniert.
  • Das Vorhandensein / Fehlen / die Reihenfolge von Anrufen bei externen Diensten.
  • Der interne Status des Dienstes unter Verwendung der Informationen, die an Testpoint übergeben werden.

Mockserver


Wir testen das Verhalten eines einzelnen Microservices. Aufrufe der HTTP-API externer Dienste sollten überbrückt werden. Für diesen Teil der Arbeit in der Testsuite treffen sich eigene Plug-Ins mockserverund mockserver_https. Mockserver ist ein HTTP-Server mit Anforderungsprozessoren, die für jeden Test und Speicher angepasst werden können, welche Anforderungen verarbeitet und welche Daten übertragen werden.

Datenbank


Mit Testsuite kann der Test zum Lesen und Schreiben direkt auf die Datenbank zugreifen. Mit den Daten können Sie eine Voraussetzung für den Test formulieren und das Ergebnis überprüfen. Standardmäßig werden PostgreSQL, MongoDB und Redis unterstützt.

Wie fange ich an?


Um Testsuite-Tests zu schreiben, muss ein Entwickler Python und das Standard- Pytest- Framework kennen .

Lassen Sie uns anhand eines einfachen Chat-Beispiels Schritt für Schritt die Verwendung der Testsuite demonstrieren. Hier sind die Quellcodes für die Anwendung und Tests.



Die Frontend- Datei chat.html interagiert mit dem Chat-Backend-Dienst .

Um zu demonstrieren, wie Dienste interagieren, delegiert das Chat-Backend den Nachrichtenspeicher an den Repository-Dienst. Die Speicherung erfolgt auf zwei Arten: Chat-Storage-Mongo und Chat-Storage-Postgres .

Chat-Backend


Der Chat-Backend-Service ist der Einstiegspunkt für Anfragen vom Frontend. Kann eine Liste von Nachrichten senden und zurückgeben .

Bedienung


Wir zeigen einen beispielhaften Anforderungshandler POST /messages/retrieve:

Quellcode

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

Tests


Bereiten Sie die Testsuite-Infrastruktur für den Start des Dienstes vor. Wir geben an, mit welchen Einstellungen wir den Dienst starten möchten.

Quelle

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

Wir setzen das Client-Fixture, durch das der Test eine HTTP-Anfrage an den Service sendet.

Quelle

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

Jetzt weiß die Infrastruktur, wie sie startet chat-backendund wie sie eine Anfrage an sie sendet. Dies reicht aus, um Tests zu schreiben.

Bitte beachten Sie, dass chat-backendwir in Tests weder Speicherdienste chat-storage-mongonoch verwenden chat-storage-postgres. Um chat-backendAnrufe normal zu verarbeiten, befeuchten wir die Speicher-API mit mockserver.

Schreiben wir einen Test für die Methode POST messages/send. Wir überprüfen Folgendes:

  • Die Anfrage wird normal bearbeitet.
  • Ruft bei der Verarbeitung der Anforderung chat-backenddie Speichermethode auf POST messages/send.

Quelle

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

Schreiben wir einen Test für die Methode POST messages/retrieve. Wir überprüfen Folgendes:

  • Die Anfrage wird normal bearbeitet.
  • Ruft bei der Verarbeitung der Anforderung chat-backenddie Speichermethode auf POST /messages/retrieve.
  • chat-backend "Spiegelt" die Liste der vom Repository empfangenen Nachrichten, sodass die neuesten Nachrichten am Ende der Liste stehen.

Quelle

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-Speicher-Postgres



Der Dienst chat-storage-postgresist für das Lesen und Schreiben von Chat-Nachrichten in die PostgreSQL-Datenbank verantwortlich.

Bedienung


So lesen wir die Liste der Nachrichten von PostgreSQL in der Methode POST /messages/retrieve:

Quellcode

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

Tests


Der Dienst, den wir testen, verwendet die PostgreSQL-Datenbank. Damit alles funktioniert, müssen Sie der Testsuite lediglich mitteilen, in welchem ​​Verzeichnis nach Tabellenschemata gesucht werden soll.

Quelle

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

Der Rest des Infrastruktur- Setups von conftest.py unterscheidet sich nicht von dem oben beschriebenen Dienst chat-backend.

Fahren wir mit den Tests fort.

Schreiben wir einen Test für die Methode POST messages/send. Überprüfen Sie, ob die Nachricht in der Datenbank gespeichert wird.

Quelle

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

Schreiben wir einen Test für die Methode POST messages/retrieve. Überprüfen Sie, ob Nachrichten aus der Datenbank zurückgegeben werden.

Erstellen Sie zunächst ein Skript, das die benötigten Datensätze zur Tabelle hinzufügt. Testsuite führt das Skript vor dem Test automatisch aus.

Quelle

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

Quelle

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

Starten


Das Ausführen von Beispielen ist in einem Docker-Container am einfachsten. Dazu müssen Docker und Docker-Compose auf dem Computer installiert sein.

Alle Beispiele werden aus dem Start-Chat- Verzeichnis gestartet.docs/examples



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

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

Nach dem Start wird die URL in der Konsole angezeigt, wo Sie den Chat im Browser öffnen können:

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

Führen Sie Tests durch

#    
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

Dokumentation


Eine ausführliche Dokumentation zur Testsuite finden Sie hier .

Anweisungen zum Einrichten und Ausführen von Beispielen.

Wenn Sie Fragen haben github.com/yandex/yandex-taxi-testsuite/issues - hinterlassen Sie einen Kommentar.

All Articles