Eine weitere Scheinbibliothek

Guten Tag. Ich beschäftige mich mit Testautomatisierung. Wie alle Automatisierungstechniker habe ich eine Reihe von Bibliotheken und Tools, die ich normalerweise zum Schreiben von Tests auswähle. Es gibt jedoch Situationen, in denen keine der bekannten Bibliotheken das Problem mit dem Risiko lösen kann, dass Autotests instabil oder zerbrechlich werden. In diesem Artikel möchte ich Ihnen sagen, wie die scheinbar übliche Aufgabe, mock'ov zu verwenden, mich veranlasste, mein Modul zu schreiben. Ich möchte auch meine Entscheidung teilen und Feedback hören.


App


Einer der notwendigen Sektoren im Finanzsektor ist die Prüfung. Die Daten müssen regelmäßig überprüft werden (Abgleich). In diesem Zusammenhang ist die von mir getestete Anwendung erschienen. Um nicht über etwas Abstraktes zu sprechen, stellen wir uns vor, dass unser Team eine Anwendung für die Verarbeitung von Anwendungen aus Instant Messenger entwickelt. Für jede Anwendung muss in elasticsearch ein entsprechendes Ereignis angelegt werden. Die Verifizierungsanwendung wird unsere Überwachung sein, dass Anwendungen nicht übersprungen werden.


Stellen Sie sich vor, wir haben ein System mit folgenden Komponenten:


  1. Konfigurationsserver. Für den Benutzer ist dies ein einziger Einstiegspunkt, an dem er nicht nur die Anwendung zur Überprüfung, sondern auch andere Komponenten des Systems konfiguriert.
  2. Verifizierungsantrag.
  3. Daten aus der Bewerbung verarbeiten Bewerbungen, die in elasticsearch gespeichert sind.
  4. Referenzdaten. Das Datenformat hängt von dem Messenger ab, in den die Anwendung integriert ist.

Herausforderung


Das Testen der Automatisierung sieht in diesem Fall ganz einfach aus:


  1. Umgebungsvorbereitung:
    • Elasticsearch wird mit minimaler Konfiguration installiert (unter Verwendung von msi und der Befehlszeile).
    • Eine Verifizierungsanwendung ist installiert.
  2. Testausführung:
    • Eine Bestätigungsanwendung ist konfiguriert.
    • Elasticsearch wird mit Testdaten für den entsprechenden Test gefüllt (wie viele Bewerbungen wurden bearbeitet).
    • Die Anwendung empfängt "Referenz" -Daten vom Messenger (wie viele Anwendungen waren angeblich tatsächlich).
    • Das Urteil des Antrags wird überprüft: Anzahl der erfolgreich verifizierten Anträge, Anzahl der fehlenden Anträge usw.
  3. Die Umwelt reinigen.

Das Problem ist, dass wir die Überwachung testen. Zum Konfigurieren benötigen wir jedoch Daten vom Konfigurationsserver. Erstens ist das Installieren und Konfigurieren eines Servers für jeden Lauf zeitaufwändig (es hat beispielsweise eine eigene Basis). Zweitens möchte ich Anwendungen isolieren, um die Lokalisierung von Problemen beim Auffinden eines Fehlers zu vereinfachen. Am Ende entschied man sich für die Verwendung von Mock.


Dies kann die Frage aufwerfen: "Wenn wir den Server immer noch verspotten, können wir möglicherweise keine Zeit damit verbringen, Elasticsearch zu installieren und zu füllen, sondern die Verspottung zu ersetzen?" Sie müssen sich jedoch immer daran erinnern, dass die Verwendung von Mock Flexibilität bietet, aber eine Verpflichtung hinzufügt, die Relevanz des Mock-Verhaltens zu überwachen. Deshalb habe ich mich geweigert, elasticsearch zu ersetzen: es ist einfach zu installieren und zu füllen.


Erster Spott


Der Server sendet die Konfiguration auf verschiedene Arten an GET-Anforderungen in / configuration. Wir sind in zweierlei Hinsicht interessiert. Das erste ist /configuration/data_cluster mit Cluster-Konfiguration


 { "host": "127.0.0.1", "port": 443, "credentials": { "username": "user", "password": "pass" } } 

Die zweite ist /configuration/reconciliation mit der Konfiguration der Bohranwendung


 { "reconciliation_interval": 3600, "configuration_update_interval": 60, "source": { "address": "file:///c:/path", "credentials": { "username": "user", "password": "pass" } } } 

Die Schwierigkeit besteht darin, dass Sie in der Lage sein müssen, die Serverantwort während des Tests oder zwischen Tests zu ändern, um zu testen, wie die Anwendung auf Konfigurationsänderungen, falsche Kennwörter usw. reagiert.


Statische Mocks und Tools für Mocks in Komponententests (Mock, Monkeypatch von Pytest usw.) funktionieren bei uns nicht. Ich fand eine großartige pretenders Bibliothek, die ich für richtig hielt. Pretenders bietet die Möglichkeit, einen HTTP-Server mit Regeln zu erstellen, die festlegen, wie der Server auf Anforderungen reagiert. Regeln werden in Voreinstellungen gespeichert, die das Isolieren von Mocks für verschiedene Testsuiten ermöglichen. Voreinstellungen können gelöscht und neu gefüllt werden, sodass Sie die Antworten nach Bedarf aktualisieren können. Es reicht aus, den Server selbst einmal während der Vorbereitung der Umgebung hochzufahren:


 python -m pretenders.server.server --host 127.0.0.1 --port 8000 

Und in den Tests müssen wir die Verwendung des Clients hinzufügen. Im einfachsten Fall kann es so aussehen, wenn die Antworten in den Tests vollständig fest codiert sind:


 import json import pytest from pretenders.client.http import HTTPMock from pretenders.common.constants import FOREVER @pytest.fixture def configuration_server_mock(request): mock = HTTPMock(host="127.0.0.1", port=8000, name="server") request.addfinalizer(mock.reset) return mock def test_something(configuration_server_mock): configuration_server_mock.when("GET /configuration/data_cluster").reply( headers={"Content-Type": "application/json"}, body=json.dumps({ "host": "127.0.0.1", "port": 443, "credentials": { "username": "user", "password": "pass", }, }), status=200, times=FOREVER, ) configuration_server_mock.when("GET /configuration/reconciliation").reply( headers={"Content-Type": "application/json"}, body=json.dumps({ "reconciliation_interval": 3600, "configuration_update_interval": 60, "source": { "address": "file:///c:/path", "credentials": { "username": "user", "password": "pass", }, }, }), status=200, times=FOREVER, ) # test application 

Das ist aber noch nicht alles. Bei der Flexibilität von pretenders gibt es zwei Einschränkungen, die beachtet und in unserem Fall berücksichtigt werden müssen:


  1. Regeln können nicht einzeln gelöscht werden. Um die Antwort zu ändern, müssen Sie die gesamte Voreinstellung löschen und alle Regeln erneut erstellen.
  2. Alle in den Regeln verwendeten Pfade sind relativ. Voreinstellungen haben einen eindeutigen Pfad in der Form / mockhttp / <Name_der_Voreinstellung>. Dieser Pfad ist ein gemeinsames Präfix für alle in den Regeln erstellten Pfade. Die getestete Anwendung erhält nur den Hostnamen und kann das Präfix nicht kennen.

Die erste Einschränkung ist sehr unangenehm, kann jedoch gelöst werden, indem ein Modul geschrieben wird, das die Arbeit mit der Konfiguration zusammenfasst. Zum Beispiel so


 configuration.data_cluster.port = 443 

oder (um Aktualisierungsanforderungen seltener zu stellen)


 data_cluster_config = get_default_data_cluster_config() data_cluster_config.port = 443 configuration.update_data_cluster(data_cluster_config) 

Eine solche Kapselung ermöglicht es uns, alle Pfade nahezu schmerzlos zu aktualisieren. Sie können auch eine individuelle Voreinstellung für jeden einzelnen Endpunkt und eine allgemeine (Haupt-) Voreinstellung erstellen, die (bis 307 oder 308) auf einzelne umleitet. Dann können Sie nur eine Voreinstellung löschen, um die Regel zu aktualisieren.


Um Präfixe loszuwerden, können Sie die mitmproxy- Bibliothek verwenden. Dies ist ein leistungsstarkes Tool, mit dem unter anderem Anforderungen umgeleitet werden können. Wir werden die Präfixe wie folgt entfernen:


 mitmdump --mode reverse:http://127.0.0.1:8000 --replacements :~http:^/:/mockhttp/server/ --listen-host 127.0.01 --listen-port 80 

Die Parameter dieses Befehls bewirken Folgendes:


  1. --listen-host 127.0.0.1 und --listen-port 80 Hand. Mitmproxy erhöht seinen Server und mit diesen Parametern bestimmen wir die Schnittstelle und den Port, die dieser Server überwacht.
  2. --mode reverse:http://127.0.0.1:8000 bedeutet, dass Anforderungen an den Mitproxyserver an http://127.0.0.1:8000 umgeleitet werden. Lesen Sie hier mehr.
  3. --replacements :~http:^/:/mockhttp/server/ definiert eine Vorlage, durch die Anforderungen geändert werden. Es besteht aus drei Teilen: einem Anforderungsfilter ( ~http für HTTP-Anforderungen), einer Vorlage zum Ändern ( ^/ zum Ersetzen des /mockhttp/server ) und zum tatsächlichen Ersetzen ( /mockhttp/server ). Lesen Sie hier mehr.

In unserem Fall fügen wir allen HTTP-Anforderungen mockhttp/server und leiten sie zu http://127.0.0.1:8000 , d. H. an unsere Server-Pretender. Als Ergebnis haben wir erreicht, dass die Konfiguration jetzt mit einer GET-Anforderung an http://127.0.0.1/configuration/data_cluster abgerufen werden kann.


Im Allgemeinen war ich mit dem Design mit pretenders und mitmproxy . Bei der offensichtlichen Komplexität - immerhin 2 Server statt eines echten - besteht die Vorbereitung darin, 2 Pakete zu installieren und 2 Befehle in der Befehlszeile auszuführen, um sie zu starten. Nicht alles ist so einfach in der Verwaltung eines Mocks, aber die gesamte Komplexität liegt an nur einem Ort (Verwaltung von Presets) und wird ganz einfach und zuverlässig gelöst. Es traten jedoch neue Umstände in dem Problem auf, die mich über eine neue Lösung nachdenken ließen.


Zweiter Schein


Bis zu diesem Moment habe ich fast nicht gesagt, woher die Referenzdaten stammen. Ein aufmerksamer Leser kann feststellen, dass im obigen Beispiel der Pfad zum Dateisystem als Adresse der Datenquelle verwendet wird. Und es funktioniert wirklich so etwas, aber nur für einen der Anbieter. Ein anderer Anbieter stellt eine API zum Empfangen von Anwendungen bereit, und bei ihm trat ein Problem auf. Da es schwierig ist, die Hersteller-API während der Tests zu erhöhen, wollte ich sie nach demselben Schema wie zuvor durch eine Schein-API ersetzen. Aber um Bewerbungen zu erhalten, eine Anfrage des Formulars


 GET /application-history?page=2&size=5&start=1569148012&end=1569148446 

Hier gibt es 2 Punkte. Erstens ein paar Möglichkeiten. Tatsache ist, dass Parameter in beliebiger Reihenfolge angegeben werden können, was den regulären Ausdruck für die pretenders Regel erheblich verkompliziert. Es muss auch beachtet werden, dass die Parameter optional sind, dies ist jedoch kein Problem wie die zufällige Reihenfolge. Zweitens geben die letzten Parameter (Anfang und Ende) das Zeitintervall zum Filtern der Reihenfolge an. Und das Problem in diesem Fall ist, dass wir nicht im Voraus vorhersagen können, welches Intervall (nicht die Größe, sondern die Startzeit) von der Anwendung zur Bildung der Scheinantwort verwendet wird. Einfach ausgedrückt müssen wir die Parameterwerte kennen und verwenden, um eine „vernünftige“ Antwort zu bilden. "Reasonability" ist in diesem Fall beispielsweise wichtig, damit wir testen können, ob die Anwendung alle Seiten der Paginierung durchläuft. Wenn wir alle Anfragen auf die gleiche Weise beantworten, können wir keine Fehler finden, da nur eine von fünf Seiten angefordert wird .


Ich habe versucht, nach alternativen Lösungen zu suchen, aber am Ende habe ich mich entschlossen, meine eigenen zu schreiben. Es gab also einen losen Server . Dies ist eine Flask- Anwendung, in der Pfade und Antworten nach dem Start konfiguriert werden können. Er kann sofort mit Regeln für die Art der Anforderung (GET, POST usw.) und für den Pfad arbeiten. Auf diese Weise können Sie pretenders und mitmproxy in der ursprünglichen Aufgabe ersetzen. Ich werde auch zeigen, wie damit ein Mock für die Vendor-API erstellt werden kann.


Eine Anwendung benötigt 2 Hauptpfade:


  1. Basisendpunkt. Dies ist das gleiche Präfix, das für alle konfigurierten Regeln verwendet wird.
  2. Konfigurationsendpunkt. Dies ist das Präfix der Anfragen, mit denen Sie den Mock-Server selbst konfigurieren können.

 python -m looseserver.default.server.run --host 127.0.0.1 --port 80 --base-endpoint / --configuration-endpoint /_mock_configuration/ 

Im Allgemeinen ist es am besten, den Basisendpunkt und den Konfigurationsendpunkt nicht so zu konfigurieren, dass einer der Eltern des anderen ist. Andernfalls besteht die Gefahr, dass die Pfade zur Konfiguration und zum Testen in Konflikt geraten. Der Konfigurationsendpunkt hat Vorrang, da Kolbenregeln für die Konfiguration früher als für dynamische Pfade hinzugefügt werden. In unserem Fall könnten wir --base-endpoint /configuration/ wenn wir die Hersteller-API nicht in dieses Modell aufnehmen würden.


Die einfachste Version der Tests ändert nicht viel


 import json import pytest from looseserver.default.client.http import HTTPClient from looseserver.default.client.rule import PathRule from looseserver.default.client.response import FixedResponse @pytest.fixture def configuration_server_mock(request): class MockFactory: def __init__(self): self._client = HTTPClient(configuration_url="http://127.0.0.1/_mock_configuration/") self._rule_ids = [] def create_rule(self, path, json_response): rule = self._client.create_rule(PathRule(path=path)) self._rule_ids.append(rule.rule_id) response = FixedResponse( headers={"Content-Type": "application/json"}, status=200, body=json.dumps(json_response), ) self._client.set_response(rule_id=rule.rule_id, response=response) def _delete_rules(self): for rule_id in self._rule_ids: self._client.remove_rule(rule_id=rule_id) mock = MockFactory() request.addfinalizer(mock._delete_rules) return mock def test_something(configuration_server_mock): configuration_server_mock.create_rule( path="configuration/data_cluster", json_response={ "host": "127.0.0.1", "port": 443, "credentials": { "username": "user", "password": "pass", }, } ) configuration_server_mock.create_rule( path="configuration/reconciliation", json_response={ "reconciliation_interval": 3600, "configuration_update_interval": 60, "source": { "address": "file:///applications", "credentials": { "username": "user", "password": "pass", }, }, } ) 

Fixture ist schwieriger geworden, aber die Regeln können jetzt einzeln gelöscht werden, was die Arbeit mit ihnen vereinfacht. Die Verwendung von mitmproxy nicht mehr erforderlich.


Kehren wir zur Hersteller-API zurück. Wir werden einen neuen Regeltyp für einen losen Server erstellen, der je nach Parameterwert unterschiedliche Antworten liefert. Als nächstes werden wir diese Regel für den Seitenparameter verwenden.


Neue Regeln und Antworten müssen sowohl für den Server als auch für den Client erstellt werden. Beginnen wir mit dem Server:


 from looseserver.server.rule import ServerRule class ServerParameterRule(ServerRule): def __init__(self, parameter_name, parameter_value=None, rule_type="PARAMETER"): super(ServerParameterRule, self).__init__(rule_type=rule_type) self._parameter_name = parameter_name self._parameter_value = parameter_value def is_match_found(self, request): if self._parameter_value is None: return self._parameter_name not in request.args return request.args.get(self._parameter_name) == self._parameter_value 

Jede Regel muss eine is_match_found Methode definieren, die festlegt, ob sie für eine bestimmte Anforderung funktionieren soll oder nicht. Der Eingabeparameter dafür ist das Anforderungsobjekt. Nachdem die neue Regel erstellt wurde, muss der Server "angelernt" werden, um sie vom Client zu akzeptieren. Verwenden Sie dazu RuleFactory :


 from looseserver.default.server.rule import create_rule_factory from looseserver.default.server.application import configure_application def _create_application(base_endpoint, configuration_endpoint): server_rule_factory = create_rule_factory(base_endpoint) def _parse_param_rule(rule_type, parameters): return ServerParameterRule( rule_type=rule_type, parameter_name=parameters["parameter_name"], parameter_value=parameters["parameter_value"], ) server_rule_factory.register_rule( rule_type="PARAMETER", parser=_parse_param_rule, serializer=lambda rule_type, rule: None, ) return configure_application( rule_factory=server_rule_factory, base_endpoint=base_endpoint, configuration_endpoint=configuration_endpoint, ) if __name__ == "__main__": application = _create_application(base_endpoint="/", configuration_endpoint="/_mock_configuration") application.run(host="127.0.0.1", port=80) 

Hier erstellen wir standardmäßig eine Factory für die Regeln, sodass sie die zuvor verwendeten Regeln enthält, und registrieren einen neuen Typ. In diesem Fall benötigt der Client die Regelinformationen nicht, sodass der serializer tatsächlich nichts unternimmt. Weiterhin wird diese Fabrik in die Anwendung übertragen. Und es kann bereits wie eine normale Flask-Anwendung ausgeführt werden.


Die Situation beim Kunden ist ähnlich: Wir erstellen eine Regel und eine Fabrik. Für den Client ist es jedoch nicht erforderlich, erstens die Methode is_match_found zu definieren, und zweitens ist in diesem Fall der Serializer erforderlich, um die Regel an den Server zu senden.


 from looseserver.client.rule import ClientRule from looseserver.default.client.rule import create_rule_factory class ClientParameterRule(ClientRule): def __init__(self, parameter_name, parameter_value=None, rule_type="PARAMETER", rule_id=None): super(ClientParameterRule, self).__init__(rule_type=rule_type, rule_id=rule_id) self.parameter_name = parameter_name self.parameter_value = parameter_value def _create_client(configuration_url): def _serialize_param_rule(rule): return { "parameter_name": rule.parameter_name, "parameter_value": rule.parameter_value, } client_rule_factory = create_rule_factory() client_rule_factory.register_rule( rule_type="PARAMETER", parser=lambda rule_type, parameters: ClientParameterRule(rule_type=rule_type, parameter_name=None), serializer=_serialize_param_rule, ) return HTTPClient(configuration_url=configuration_url, rule_factory=client_rule_factory) 

Es bleibt _create_client zu verwenden, um den Client zu erstellen, und die Regeln können in Tests verwendet werden. Im folgenden Beispiel habe ich die Verwendung einer anderen Standardregel hinzugefügt: CompositeRule . Sie können mehrere Regeln zu einer kombinieren, sodass sie nur funktionieren, wenn jede von ihnen beim Aufruf von is_match_found True is_match_found .


 @pytest.fixture def configuration_server_mock(request): class MockFactory: def __init__(self): self._client = _create_client("http://127.0.0.1/_mock_configuration/") self._rule_ids = [] def create_paged_rule(self, path, page, json_response): rule_prototype = CompositeRule( children=[ PathRule(path=path), ClientParameterRule(parameter_name="page", parameter_value=page), ] ) rule = self._client.create_rule(rule_prototype) self._rule_ids.append(rule.rule_id) response = FixedResponse( headers={"Content-Type": "application/json"}, status=200, body=json.dumps(json_response), ) self._client.set_response(rule_id=rule.rule_id, response=response) ... mock = MockFactory() request.addfinalizer(mock._delete_rules) return mock def test_something(configuration_server_mock): ... configuration_server_mock.create_paged_rule( path="application-history", page=None, json_response=["1", "2", "3"], ) configuration_server_mock.create_paged_rule( path="application-history", page="1", json_response=["1", "2", "3"], ) configuration_server_mock.create_paged_rule( path="application-history", page="2", json_response=["4", "5"], ) 

Fazit


Eine mitmproxy pretenders und mitmproxy bietet ein leistungsstarkes und ausreichend flexibles Werkzeug zum Erstellen von Mocks. Ihre Vorteile:


  1. Einfaches Setup.
  2. Möglichkeit, Abfragesätze mithilfe von Vorgaben zu isolieren.
  3. Löscht einen gesamten isolierten Satz auf einmal.

Durch die Nachteile gehören:


  1. Die Notwendigkeit, reguläre Ausdrücke für Regeln zu erstellen.
  2. Die Unfähigkeit, die Regeln individuell zu ändern.
  3. Das Vorhandensein eines Präfixes für alle erstellten Pfade oder die Verwendung der Umleitung mithilfe von mitmproxy .

Dokumentationslinks:
Prätendenten
Mitmproxy
Lose Server

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


All Articles