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: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.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 mockserver
et 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
@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
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,
service_client_options,
ensure_daemon_started,
mockserver,
):
await ensure_daemon_started(service_daemon)
yield service_client.Client(SERVICE_BASEURL, **service_client_options)
L'infrastructure sait maintenant comment démarrer chat-backend
et comment lui envoyer une demande. Cela suffit pour commencer à écrire des tests.Veuillez noter que dans les tests, chat-backend
nous n'utilisons pas les services de stockage, ni chat-storage-mongo
, ni chat-storage-postgres
. Pour chat-backend
gé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-backend
appelle la méthode de stockage POST messages/send
.
La sourceasync 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
É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-backend
appelle 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 sourceasync 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-stockage-postgres
Le service chat-storage-postgres
est 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 sourceasync 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')
É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
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
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',
},
],
}
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
docs/examples$ make run-chat-mongo
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.