Testmethoden von `test::nginx`: Konfiguration, Senden von Anfragen und Verarbeiten von Antworten

API7.ai

November 18, 2022

OpenResty (NGINX + Lua)

Im letzten Artikel haben wir bereits einen ersten Blick auf test::nginx geworfen und das einfachste Beispiel ausgeführt. In einem echten Open-Source-Projekt sind die mit test::nginx geschriebenen Testfälle jedoch viel komplexer und schwieriger zu beherrschen als der Beispielcode. Sonst wäre es kein Hindernis.

In diesem Artikel werde ich Sie durch die häufig verwendeten Befehle und Testmethoden in test::nginx führen, damit Sie die meisten Testfallsätze im OpenResty-Projekt verstehen und in der Lage sind, realistischere Testfälle zu schreiben. Selbst wenn Sie noch keinen Code zu OpenResty beigetragen haben, wird die Vertrautheit mit dem Testframework von OpenResty eine große Inspiration für Sie sein, um Testfälle in Ihrer Arbeit zu entwerfen und zu schreiben.

Der test::nginx-Test generiert im Wesentlichen eine nginx.conf und startet einen NGINX-Prozess basierend auf der Konfiguration jedes Testfalls. Dann simuliert er eine Client-Anfrage mit dem angegebenen Anfragekörper und den Headern. Anschließend verarbeitet der Lua-Code im Testfall die Anfrage und gibt eine Antwort. Zu diesem Zeitpunkt analysiert test::nginx die kritischen Informationen wie den Antwortkörper, die Antwortheader und die Fehlerprotokolle und vergleicht sie mit der Testkonfiguration. Wenn eine Diskrepanz besteht, schlägt der Test mit einem Fehler fehl; andernfalls ist er erfolgreich.

test::nginx bietet viele DSL (Domain-spezifische Sprache)-Primitive. Ich habe eine einfache Klassifizierung vorgenommen, basierend auf der Konfiguration von NGINX, dem Senden von Anfragen, der Verarbeitung von Antworten und der Überprüfung von Protokollen. Diese 20 % der Funktionalität können 80 % der Anwendungsfälle abdecken, daher müssen wir sie fest im Griff haben. Andere fortgeschrittenere Primitive und Verwendungen werden wir im nächsten Artikel vorstellen.

NGINX-Konfiguration

Schauen wir uns zunächst die NGINX-Konfiguration an. Das Primitive von test::nginx mit dem Schlüsselwort "config" bezieht sich auf die NGINX-Konfiguration, wie z.B. config, stream_config, http_config usw.

Ihre Funktionen sind dieselben: Sie fügen die angegebene NGINX-Konfiguration in verschiedene NGINX-Kontexte ein. Diese Konfigurationen können entweder NGINX-Befehle oder in content_by_lua_block gekapselter Lua-Code sein.

Bei Unit-Tests ist config das am häufigsten verwendete Primitive, in dem wir Lua-Bibliotheken laden und Funktionen für White-Box-Tests aufrufen. Hier ist ein Testcode-Snippet, das nicht vollständig ausgeführt werden kann. Es stammt aus einem echten Open-Source-Projekt. Wenn Sie interessiert sind, können Sie auf den Link klicken, um den vollständigen Test zu sehen, oder Sie können versuchen, ihn lokal auszuführen.

=== TEST 1: sanity
--- config
    location /t {
        content_by_lua_block {
            local plugin = require("apisix.plugins.key-auth")
            local ok, err = plugin.check_schema({key = 'test-key'})
            if not ok then
                ngx.say(err)
            end
            ngx.say("done")
        }
    }

Der Zweck dieses Testfalls ist es, zu testen, ob die Funktion check_schema in der Codedatei plugins.key-auth ordnungsgemäß funktioniert. Es verwendet den NGINX-Befehl content_by_lua_block in location /t, um das zu testende Modul zu laden und die zu überprüfende Funktion direkt aufzurufen.

Dies ist ein gängiges Mittel für White-Box-Tests in test::nginx. Diese Konfiguration allein reicht jedoch nicht aus, um den Test abzuschließen. Lassen Sie uns also fortfahren und sehen, wie eine Client-Anfrage gesendet wird.

Anfragen senden

Das Simulieren eines Clients, der eine Anfrage sendet, beinhaltet viele Details. Beginnen wir mit dem einfachsten - dem Senden einer einzelnen Anfrage.

request

Fahren wir mit dem obigen Testfall fort. Wenn wir möchten, dass der Unit-Test-Code ausgeführt wird, müssen wir eine HTTP-Anfrage an die in der Konfiguration angegebene Adresse /t senden, wie im folgenden Testcode gezeigt:

--- request
GET /t

Dieser Code sendet eine GET-Anfrage an /t im Anfrage-Primitive. Hier geben wir nicht die IP-Adresse, den Domainnamen oder den Port des Zugriffs an, noch geben wir an, ob es sich um HTTP 1.0 oder HTTP 1.1 handelt. All diese Details werden von test::nginx verborgen, sodass wir uns nicht darum kümmern müssen. Dies ist einer der Vorteile von DSL - wir müssen uns nur auf die Geschäftslogik konzentrieren, ohne von allen Details abgelenkt zu werden.

Außerdem bietet dies teilweise Flexibilität. Zum Beispiel ist das Standardprotokoll HTTP 1.1, oder wenn wir HTTP 1.0 testen möchten, können wir dies separat angeben:

--- request
GET /t  HTTP/1.0

Neben der GET-Methode muss auch die POST-Methode unterstützt werden. Im folgenden Beispiel können wir die Zeichenkette hello world an die angegebene Adresse POSTen.

--- request
POST /t  
hello world

Auch hier berechnet test::nginx die Länge des Anfragekörpers für Sie und fügt die host- und connection-Anfrageheader automatisch hinzu, um sicherzustellen, dass dies eine normale Anfrage ist.

Natürlich können wir Kommentare hinzufügen, um es lesbarer zu machen. Zeilen, die mit # beginnen, werden als Codekommentare erkannt.

--- request
# post request
POST /t  
hello world

Die Anfrage unterstützt auch einen komplexeren und flexibleren Modus, der eval als Filter verwendet, um Perl-Code direkt einzubetten, da test::nginx in Perl geschrieben ist. Wenn die aktuelle DSL-Sprache Ihre Anforderungen nicht erfüllt, ist eval die "ultimative Waffe", um Perl-Code direkt auszuführen.

Für die Verwendung von eval schauen wir uns hier ein paar einfache Beispiele an, und wir werden im nächsten Artikel mit anderen komplexeren fortfahren.

--- request eval
"POST /t
hello\x00\x01\x02
world\x03\x04\xff"

Im ersten Beispiel verwenden wir eval, um nicht druckbare Zeichen anzugeben, was eine seiner Verwendungen ist. Der Inhalt zwischen den doppelten Anführungszeichen wird als Perl-String behandelt und dann als Argument an die request übergeben.

Hier ist ein interessanteres Beispiel:

--- request eval
"POST /t\n" . "a" x 1024

Um dieses Beispiel zu verstehen, müssen wir jedoch etwas über Strings in Perl wissen, daher möchte ich hier kurz zwei Punkte erwähnen.

  • In Perl verwenden wir einen Punkt, um String-Verkettung darzustellen. Ist das nicht ein bisschen ähnlich wie die zwei Punkte in Lua?
  • Ein kleines x gibt die Anzahl der Wiederholungen eines Zeichens an. Zum Beispiel bedeutet "a" x 1024 oben, dass das Zeichen "a" 1024 Mal wiederholt wird.

Das zweite Beispiel bedeutet also, dass die POST-Methode eine Anfrage mit 1024 Zeichen a an die Adresse /t sendet.

pipelined_requests

Nachdem wir verstanden haben, wie eine einzelne Anfrage gesendet wird, schauen wir uns an, wie mehrere Anfragen gesendet werden. In test::nginx können wir das Primitive pipelined_requests verwenden, um mehrere Anfragen nacheinander innerhalb derselben keep-alive-Verbindung zu senden:

--- pipelined_requests eval
["GET /hello", "GET /world", "GET /foo", "GET /bar"]

Dieses Beispiel greift beispielsweise nacheinander auf diese vier APIs in derselben Verbindung zu. Es gibt zwei Vorteile dabei:

  • Der erste ist, dass viel repetitiver Testcode eliminiert werden kann und die vier Testfälle in einen komprimiert werden können.
  • Der zweite und wichtigste Grund ist, dass wir mit pipelined requests feststellen können, ob die Codelogik bei mehrfachen Zugriffen Ausnahmen aufweist.

Sie fragen sich vielleicht, wenn ich mehrere Testfälle nacheinander schreibe, wird der Code dann nicht auch mehrmals in der Ausführungsphase ausgeführt? Deckt das nicht auch das zweite Problem oben ab?

Es kommt auf den Ausführungsmodus von test::nginx an, der anders funktioniert, als Sie vielleicht denken. Nach jedem Testfall schaltet test::nginx den aktuellen NGINX-Prozess ab, und alle Daten im Speicher verschwinden. Beim Ausführen des nächsten Testfalls wird die nginx.conf neu generiert und ein neuer NGINX-Worker gestartet. Dieser Mechanismus stellt sicher, dass sich Testfälle nicht gegenseitig beeinflussen.

Wenn wir also mehrere Anfragen testen möchten, müssen wir das Primitive pipelined_requests verwenden. Basierend darauf können wir Szenarien wie Ratenbegrenzung, Parallelitätsbegrenzung und viele andere simulieren, um zu testen, ob Ihr System unter realistischeren und komplexeren Szenarien ordnungsgemäß funktioniert. Wir werden dies ebenfalls im näch Artikel behandeln, da es mehrere Befehle und Primitive beinhalten wird.

repeat_each

Wir haben gerade den Fall des Testens mehrerer Anfragen erwähnt. Wie sollten wir denselben Test mehrmals ausführen?

Für dieses Problem bietet test::nginx eine globale Einstellung: repeat_each, eine Perl-Funktion, die standardmäßig auf repeat_each(1) gesetzt ist, was bedeutet, dass der Testfall nur einmal ausgeführt wird. Daher müssen wir in den vorherigen Testfällen dies nicht separat festlegen.

Natürlich können wir es vor der Funktion run_test() festlegen, indem wir das Argument beispielsweise auf 2 ändern.

repeat_each(2);
run_tests();

Dann wird jeder Testfall zweimal ausgeführt, und so weiter.

more_headers

Nachdem wir über den Anfragekörper gesprochen haben, schauen wir uns die Anfrageheader an. Wie wir oben erwähnt haben, sendet test::nginx die Anfrage standardmäßig mit den Headern host und connection. Was ist mit den anderen Anfrageheadern?

more_headers ist speziell dafür gedacht.

--- more_headers
X-Foo: blah

Wir können es verwenden, um verschiedene benutzerdefinierte Header zu setzen. Wenn wir mehr als einen Header setzen möchten, setzen wir mehr als eine Zeile:

--- more_headers
X-Foo: 3
User-Agent: openresty

Antworten verarbeiten

Nach dem Senden der Anfrage ist der wichtigste Teil von test::nginx die Verarbeitung der Antwort, wo wir feststellen, ob die Antwort den Erwartungen entspricht. Hier teilen wir es in vier Teile auf und stellen sie vor: den Antwortkörper, die Antwortheader, den Antwortstatuscode und das Protokoll.

response_body

Das Gegenstück zum Anfrage-Primitive ist response_body, und das Folgende ist ein Beispiel für ihre beiden Konfigurationen in der Verwendung:

=== TEST 1: sanity
--- config
    location /t {
        content_by_lua_block {
            ngx.say("hello")
        }
    }
--- request
GET /t
--- response_body
hello

Dieser Testfall wird bestanden, wenn der Antwortkörper hello ist, und wird in anderen Fällen einen Fehler melden. Aber wie testen wir einen langen Rückgabekörper? Machen Sie sich keine Sorgen, test::nginx hat dies bereits für Sie erledigt. Es unterstützt die Erkennung des Antwortkörpers mit einem regulären Ausdruck, wie im Folgenden:

--- response_body_like
^he\w+$

Dies ermöglicht es Ihnen, sehr flexibel mit dem Antwortkörper umzugehen. Darüber hinaus unterstützt test::nginx auch unlike-Operationen:

--- response_body_unlike
^he\w+$

An diesem Punkt wird der Test nicht bestanden, wenn der Antwortkörper hello ist.

In der gleichen Weise, nachdem wir die Erkennung einer einzelnen Anfrage verstanden haben, schauen wir uns die Erkennung mehrerer Anfragen an. Hier ist ein Beispiel, wie es mit pipelined_requests verwendet wird:

--- pipelined_requests eval
["GET /hello", "GET /world", "GET /foo", "GET /bar"]
--- response_body eval
["hello", "world", "oo", "bar"]

Natürlich ist hier wichtig zu beachten, dass Sie so viele Antworten benötigen, wie Sie Anfragen senden.

response_headers

Zweitens sprechen wir über die Antwortheader. Die Antwortheader ähneln den Anfrageheadern, da jede Zeile dem Schlüssel und Wert eines Headers entspricht.

--- response_headers
X-RateLimit-Limit: 2
X-RateLimit-Remaining: 1

Ähnlich wie bei der Erkennung des Antwortkörpers unterstützen Antwortheader auch reguläre Ausdrücke und unlike-Operationen, wie response_headers_like, raw_response_headers_like und raw_response_headers_unlike.

error_code

Der dritte ist der Antwortcode. Die Erkennung des Antwortcodes unterstützt den direkten Vergleich und auch like-Operationen, wie die folgenden beiden Beispiele:

--- error_code: 302
--- error_code_like: ^(?:500)?$

Im Fall mehrerer Anfragen muss der error_code mehrmals überprüft werden:

--- pipelined_requests eval
["GET /hello", "GET /hello", "GET /hello", "GET /hello"]
--- error_code eval
[200, 200, 503, 503]

error_log

Der letzte Testpunkt ist das Fehlerprotokoll. In den meisten Testfällen wird kein Fehlerprotokoll generiert. Wir können no_error_log zur Erkennung verwenden:

--- no_error_log
[error]

Im obigen Beispiel schlägt der Test fehl, wenn die Zeichenkette [error] im NGINX error.log erscheint. Dies ist eine sehr häufige Funktion, und es wird empfohlen, dass Sie die Erkennung des Fehlerprotokolls zu allen Ihren normalen Tests hinzufügen.

--- error_log
hello world

Die obige Konfiguration erkennt das Vorhandensein von hello world in error.log. Natürlich können Sie eval verwenden, um Perl-Code einzubetten und reguläre Ausdrücke zur Erkennung zu implementieren, wie im Folgenden:

--- error_log eval
qr/\[notice\] .*?  \d+ hello world/

Zusammenfassung

Heute haben wir gelernt, wie man Anfragen sendet und Antworten in test::nginx testet, einschließlich des Anfragekörpers, der Header, des Antwortstatuscodes und des Fehlerprotokolls. Mit der Kombination dieser Primitive können wir einen vollständigen Satz von Testfällen implementieren.

Abschließend hier eine Denkaufgabe: Was sind die Vor- und Nachteile von test::nginx, einer abstrakten DSL? Hinterlassen Sie gerne Kommentare und diskutieren Sie mit mir, und Sie sind auch eingeladen, diesen Artikel zu teilen, um gemeinsam zu kommunizieren und nachzudenken.