Gut, schlecht, böse - Testen in einem Anfängerprojekt

Vorwort: Die Universität erhielt die Aufgabe, ein Scrum-Team zusammenzustellen, ein Projekt auszuwählen und ein Semester lang daran zu arbeiten. Unser Team entschied sich für die Entwicklung von Webanwendungen (Reagieren + Kolben). In diesem Artikel werde ich versuchen, Ihnen zu sagen, welche Tests hätten durchgeführt werden sollen, und zu analysieren, was wir im Backend getan haben.



Erwartungen


Zuallererst sind Tests notwendig, um alle (einschließlich uns selbst) davon zu überzeugen, dass sich das Programm in Testsituationen so verhält, wie es sollte. Zweitens stellen sie die Leistung des Codes sicher, der in Zukunft durch Tests abgedeckt wird . Das Schreiben von Tests ist ein nützlicher Prozess, da Sie dabei häufig auf Problembereiche stoßen, sich an Extremfälle erinnern, Probleme mit Schnittstellen feststellen usw.


Bei der Entwicklung von Systemen müssen Sie sich mindestens drei Arten von Tests merken:


  • Unit-Tests sind Tests, die bestätigen, dass Funktionen das tun, was sie benötigen.
  • Integrationstests sind Tests, die bestätigen, dass mehrere Funktionen zusammen das Richtige tun.
  • Systemtests sind Tests, die überprüfen, ob das gesamte System das tut, was es benötigt.

In einem der Beiträge von Google wurde eine Tabelle mit einer Beschreibung von drei Arten von Tests veröffentlicht. "Klein", "Mittel" und "Groß".



Unit-Tests


Unit-Tests entsprechen kleinen Tests - sie sollten schnell sein und nur die Richtigkeit bestimmter Teile des Programms überprüfen. Sie sollten nicht auf die Datenbank zugreifen und nicht in komplexen Multithread-Umgebungen funktionieren. Sie kontrollieren die Einhaltung von Spezifikationen / Standards und haben häufig die Rolle von Regressionstests .


Integrationstests


Integrationstests sind Tests, die sich auf mehrere Module und Funktionen auswirken können. Solche Tests erfordern mehr Zeit und erfordern möglicherweise spezielle Umgebungen. Sie sind notwendig, um sicherzustellen, dass einzelne Module und Funktionen miteinander arbeiten können. Das heißt, Unit-Tests überprüfen die Konformität der realen Schnittstellen mit den erwarteten und Integrationstests - dass Funktionen und Module korrekt miteinander interagieren.


Systemtests


Dies ist die höchste Stufe der automatischen Prüfung. Systemtests bestätigen, dass das gesamte System funktioniert, dass seine Teile ihre Aufgaben ausführen und korrekt interagieren können.


Warum Typen im Auge behalten?


Normalerweise wächst mit dem Wachstum des Projekts die Codebasis. Die Dauer der automatischen Überprüfungen wird länger, und die Unterstützung einer großen Anzahl von Integrations- und Systemtests wird immer schwieriger. Daher besteht die Herausforderung für Entwickler darin, die erforderlichen Tests zu minimieren. Versuchen Sie dazu, nach Möglichkeit Unit-Tests zu verwenden und die Integration mithilfe von "Mocks" (Mocks) zu reduzieren.


Realität


Typischer API-Test


def test_user_reg(client): return json.loads( client.post(url, json=data, content_type='application/json').data ) response = client.post('api/user.reg', json={ 'email': 'name@mail.ru', 'password': 'password1', 'first_name': 'Name', 'last_name': 'Last Name' }) data = json.loads(response.data) assert data['code'] == 0 

Aus der offiziellen Dokumentation der Flasche erhalten wir ein fertiges Rezept für die Initialisierung der Anwendung und die Erstellung der Datenbank. Hier ist die Arbeit mit der Datenbank. Dies ist kein Komponententest, aber kein Systemtest. Dies ist ein Integrationstest, der eine Datenbanktestanwendung verwendet.


Warum eher Integration als modular? Denn bei der Abfrageverarbeitung wird die Interaktion mit flask, mit ORM und mit unserer Geschäftslogik durchgeführt. Handler fungieren als einheitliches Element für andere Teile des Projekts, sodass das Schreiben von Komponententests für sie nicht allzu einfach (Sie müssen die Datenbank durch Mocks und interne Logik ersetzen) und nicht zu praktisch ist (Integrationstests prüfen ähnliche Aspekte - "Wurden die erforderlichen Funktionen aufgerufen?", " Wurden die Daten korrekt empfangen? "Usw.).


Namen und Gruppierung von Tests


 def test_not_empty_errors(): assert validate_not_empty('email', '') == ('email is empty',) assert validate_not_empty('email', ' ') == ('email is empty',) assert validate_email_format('email', "") == ('email is empty',) assert validate_password_format('pass', "") == ('pass is empty',) assert validate_datetime('datetime', "") == ('datetime is empty',) 

In diesem Test sind alle Bedingungen für "kleine" Tests erfüllt - das Verhalten der Funktion ohne Abhängigkeiten wird auf Übereinstimmung mit den erwarteten geprüft. Das Design wirft jedoch Fragen auf.


Es wird empfohlen, Tests zu schreiben, die sich auf einen bestimmten Aspekt des Programms konzentrieren. In diesem Beispiel gibt es verschiedene Funktionen: validate_password_format , validate_password_format , validate_datetime . Gruppierungsprüfungen basieren nicht auf dem Ergebnis, sondern auf den Testobjekten.


Der Name des Tests ( test_not_empty_errors ) beschreibt nicht das Testobjekt (welche Methode getestet wird), sondern nur das Ergebnis (Fehler sind nicht leer). Diese Methode sollte test__validate_not_empty__error_on_empty heißen. Dieser Name beschreibt, was getestet wird und welches Ergebnis erwartet wird. Dies gilt für fast jeden Testnamen im Projekt, da keine Zeit für die Erörterung der Testbenennungskonventionen benötigt wurde.


Regressionstests


 def test_datetime_errors(): assert validate_datetime('datetime', '0123-24-31T;431') == ('datetime is invalid',) assert validate_datetime('datetime', '2018-10-18T20:21:21+-23:1') == ('datetime is invalid',) assert validate_datetime('datetime', '2015-13-20T20:20:20+20:20') == ('datetime is invalid',) assert validate_datetime('datetime', '2015-02-29T20:20:20+20:20') == ('datetime is invalid',) assert validate_datetime('datetime', '2015-12-20T25:20:20+20:20') == ('datetime is invalid',) assert validate_datetime('datetime', '2015-12-20T20:61:20+22:20') == ('datetime is invalid',) assert validate_datetime('datetime', '2015-12-20T20:20:61+20:20') == ('datetime is invalid',) assert validate_datetime('datetime', '2015-12-20T20:20:20+25:20') == ('datetime is invalid',) assert validate_datetime('datetime', '2015-12-20T20:20:20+20:61') == ('datetime is invalid',) assert validate_datetime('datetime', '2015-13-35T25:61:61+61:61') == ('datetime is invalid',) 

Dieser Test bestand ursprünglich aus den ersten beiden assert . Danach wurde ein "Fehler" entdeckt - anstatt das Datum zu überprüfen, wurde nur der reguläre Ausdruck überprüft, d. H. 9999-99-99 wurde als normales Datum angesehen. Der Entwickler hat das Problem behoben. Nachdem Sie den Fehler behoben haben, müssen Sie natürlich Tests hinzufügen, um eine zukünftige Regression zu verhindern. Anstatt einen neuen Test hinzuzufügen, in den geschrieben werden soll, warum dieser Test vorhanden ist, wurden diesem Test Überprüfungen hinzugefügt.


Wie sollte ein neuer Test aufgerufen werden, um eine Überprüfung hinzuzufügen? Wahrscheinlich test__validate_datetime__error_on_bad_datetime .


Werkzeuge ignorieren


 def test_get_providers(): class Tmp: def __init__(self, id_external, token, username): self.id_external = id_external self.token = token self.username = username ... 

Tmp ? Dies ist ein Ersatz für ein Objekt, das in diesem Test nicht verwendet wird. Der Entwickler scheint von @patch MagicMock über die Existenz von @patch und MagicMock zu wissen. Sie müssen den Code nicht komplizieren, indem Sie Probleme naiv lösen, wenn es geeignetere Tools gibt.


Es gibt einen solchen Test, der die Dienste (in der Datenbank) initialisiert und den Anwendungskontext verwendet.


 def test_get_posts(client): def fake_request(*args, **kwargs): return [one, two] handler = VKServiceHandler() handler.request = fake_request services_init() with app.app_context(): posts = handler.get_posts(None) assert len(posts) == 2 

Sie können Datenbank und Kontext vom Test ausschließen, indem Sie einfach ein @patch hinzufügen.


 @patch("mobius.services.service_vk.Service") def test_get_posts(mock): def fake_request(*args, **kwargs): return [one, two] handler = VKServiceHandler() handler.request = fake_request posts = handler.get_posts(None) assert len(posts) == 2 

Zusammenfassung


  • Um hochwertige Software zu entwickeln, müssen Sie Tests schreiben. Zumindest, um sicherzustellen, dass Sie schreiben, was Sie brauchen.
  • Für große Informationssysteme sind Tests noch wichtiger - sie ermöglichen es Ihnen, unerwünschte Änderungen an der Benutzeroberfläche zu vermeiden oder Fehler zurückzugeben.
  • Damit die schriftlichen Tests im Laufe der Zeit nicht zu vielen seltsamen Methoden werden, müssen Sie die Namenskonvention der Tests beachten, bewährte Verfahren einhalten und die Tests minimieren.
  • Unit-Tests können während der Entwicklung ein großartiges Werkzeug sein. Sie können nach jeder kleinen Änderung ausgeführt werden, um sicherzustellen, dass nichts kaputt ist.

Ein sehr wichtiger Punkt ist, dass die Tests nicht die Verfügbarkeit oder Abwesenheit von Fehlern garantieren. Tests stellen sicher, dass das tatsächliche Ergebnis des Programms (oder eines Teils davon) erwartet wird. In diesem Fall erfolgt die Überprüfung nur für die Aspekte, für die Tests geschrieben wurden. Daher sollten wir bei der Erstellung eines Qualitätsprodukts andere Testarten nicht vergessen.

Source: https://habr.com/ru/post/de450290/


All Articles