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: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.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 mockserver
und 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
@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
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,
service_client_options,
ensure_daemon_started,
mockserver,
):
await ensure_daemon_started(service_daemon)
yield service_client.Client(SERVICE_BASEURL, **service_client_options)
Jetzt weiß die Infrastruktur, wie sie startet chat-backend
und wie sie eine Anfrage an sie sendet. Dies reicht aus, um Tests zu schreiben.Bitte beachten Sie, dass chat-backend
wir in Tests weder Speicherdienste chat-storage-mongo
noch verwenden chat-storage-postgres
. Um chat-backend
Anrufe 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-backend
die Speichermethode auf POST messages/send
.
Quelleasync 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
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-backend
die Speichermethode auf POST /messages/retrieve
. chat-backend
"Spiegelt" die Liste der vom Repository empfangenen Nachrichten, sodass die neuesten Nachrichten am Ende der Liste stehen.
Quelleasync 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-Speicher-Postgres
Der Dienst chat-storage-postgres
ist 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.Quelleasync 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')
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
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
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',
},
],
}
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
docs/examples$ make run-chat-mongo
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.