Testen
Testen ist ein wichtiger Teil der Softwareentwicklung. In dieser Lektion werden wir uns anschauen, wie man Elixir Code mit ExUnit testet und einige best practices davon kennen lernen.
ExUnit
Elixirs eingebautes Testframework ist ExUnit und es beinhaltet alles, was wir brauchen, um unseren Code durch und durch zu testen. Bevor wir fortfahren, ist es wichtig festzuhalten, dass Tests als Elixirscripts implementiert werden, also müssen wir die .exs-Dateiendung verwenden. Bevor wir Tests laufen lassen können, müssen wir ExUnit startet mit den Aufruf ExUnit.start(), was meist in test/test_helper.exs geschieht.
Wenn wir unser Beispielprojekt in der letzten Lektion erstellt haben, hat mix bereits einfache Tests für uns generiert, die wir in test/example_test.exs finden:
defmodule ExampleTest do
use ExUnit.Case
doctest Example
test "the truth" do
assert 1 + 1 == 2
end
end
Wir können die Tests unserer Projekts mit mix test starten. Falls wir das jetzt tun, sehen wir eine Ausgabe wie hier:
Finished in 0.03 seconds (0.02s on load, 0.01s on tests)
1 tests, 0 failures
assert
Falls du schon mal Tests geschrieben hast, bist du vertraut mit assert; in manchen Frameworks übernehmen should oder expect die Rolle von assert.
Wir benutzen das assert-Makro, um zu überprüfen, ob ein Ausdruck wahr ist. Im Falle, dass dem nicht so ist, wird ein Fehler geworfen und unser Test schlägt fehl. Um einen Fehler zu testen, lass uns unser Beispiel verändern und dann mix test aufrufen:
defmodule ExampleTest do
use ExUnit.Case
doctest Example
test "the truth" do
assert 1 + 1 == 3
end
end
Dann sollten wir eine Ausgabe ähnlich wie hier sehen:
1) test the truth (ExampleTest)
test/example_test.exs:5
Assertion with == failed
code: 1 + 1 == 3
lhs: 2
rhs: 3
stacktrace:
test/example_test.exs:6
......
Finished in 0.03 seconds (0.02s on load, 0.01s on tests)
1 tests, 1 failures
ExUnit sagt uns exakt, wo unsere fehlgeschlagene Annahme ist, was der erwartete Wert ist und was der tatsächliche Wert war.
refute
refute ist zu assert das, was unless zu if ist. Benutze refute wenn du sicher stellen willst, dass ein Ausdruck immer falsch ist.
assert_raise
Manchmal kann es notwendig sein sicherzustellen, dass ein Fehler geworfen wurde. Wir können das mit assert_raise machen. Wir werden ein Beispiel von assert_raise in der nächsten Lektion zu Plug sehen.
assert_receive
In Elixir bestehen Anwendungen aus Actors/Prozessen, die sich gegenseitig Nachrichten schicken können. Daher möchtest du oft testen, ob eine Nachricht versendet wurde. Da ExUnit in seinem eigenen Prozess läuft, kann es wie jeder andere Prozess Nachrichten empfangen und du kannst mit dem assert_received-Makro asserten:
defmodule SendingProcess do
def run(pid) do
send(pid, :ping)
end
end
defmodule TestReceive do
use ExUnit.Case
test "receives ping" do
SendingProcess.run(self())
assert_received :ping
end
end
assert_received wartet nicht auf Nachrichten, mit assert_receive kannst du einen Timeout festlegen.
capture_io und capture_log
Die Ausgabe einer Anwendung aufzuzeichen ist mit ExUnit.CaptureIO möglich, ohne die Originalanwendung umzuschreiben. Gib ihr einfach nur die Funktion, die die Ausgabe generiert:
defmodule OutputTest do
use ExUnit.Case
import ExUnit.CaptureIO
test "outputs Hello World" do
assert capture_io(fn -> IO.puts("Hello World") end) == "Hello World\n"
end
end
ExUnit.CaptureLog ist das Äquivalent zu Logger, um Ausgaben aufzuzeichen.
Test Setup
Manchmal kann es notwendig sein, ein paar Setups auszuführen, bevor Tests laufen können. Um das zu bewerkstelligen können wir die setup- und setup_all-Makros nutzen. setup wird vor jedem Test aufgerufen und setup_all einmal vor der Testsuite. Es wird davon ausgegangen, dass sie ein Tupel mit {:ok, state} zurückgeben, der Zustand wird für unsere Tests verfügbar sein.
Deutlichkeitshalber werden wir unseren Code so ändern, dass er setup_all nutzt:
defmodule ExampleTest do
use ExUnit.Case
doctest Example
setup_all do
{:ok, number: 2}
end
test "the truth", state do
assert 1 + 1 == state[:number]
end
end
Mocking
Die einfache Antwort für mocking in Elixir ist: Tu es nicht. Du magst vielleicht instinktiv mocks nehmen wollen, aber sie werden aus gutem Grund von der Elixir-Community nicht benutzt.
Für eine längere Diskussion ist hier ein exzellenter Artikel. Die Zusammenfassung ist, dass statt dem mocken (mock als Verb) von Testen von Abhängigkeiten es mehr Vorteile hat, Schnittstellen für Code außerhalb deines Codes explizit zu definieren (Verhaltensweisen) und Mocks (als Nomen) in deinem Clientcode zum Testen dazu zu benutzen.
Um die Implementierung in deinem Anwendungscode zu wechseln ist der präferierte Weg ein Modul als Argument zu übergeben und einen Defaultwert zu verwenden. Falls das nicht funktioniert, benutze den eingebauten Konfigurationsmechanismus. Um dies Mock-Implementierungen zu erstellen brauchst du keine spezielle Mocking-Bibliothek, sondern einfach nur behaviours und callbacks.
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!