Tydzień temu omówiliśmy najprostszy przykład testowania kodu w elixir za pomocą przykładów w dokumentacji metody. W tym tygodniu skoncentrujemy się na tym na jakiej zasadzie to działa i jak dodać jakieś bardziej złożone testy. Warto tutaj zaznaczyć, że elixir jest językiem, który przychodzi z testami jednostkowymi i są one nieodłączną cechą języka.

Jak rozpocząć przygodę z testowaniem w elixir?

Jeżeli korzystamy z mix przy tworzeniu projektu to nic nie musimy zrobić. Zostaną za nas utworzone dwa pliki:

test_helper.exs
appname_test.exs

Pierwszy z nich będzie zawierał tylko i wyłącznie jedną metodę:

ExUnit.start

Która przygotuje i odpali nam serwer testowy. Jest to bardzo ważne, bez tego test nie zostaną wykonane.

Drugi plik zaś zawiera prosty moduł:

defmodule AppNameTest do
  use ExUnit.Case
  doctest AppName

  test "the truth" do
    assert 1 + 1 == 2
  end

end

Na samym początku, zostanie użyte makro które wciągnie nam moduł dając możliwość wykorzystania makr w nim zawartych (tak zwane require) oraz zostanie wykonana metoda która skonfiguruje nasz moduł – zaimplementuje domyślne callbacki itp. Dzięki czemu nie musimy prawie nic innego pisać. Co lepsze metoda __using__, zaimportuje nam makra które umożliwią tworzenie testów i sprawdzanie wartości – umożliwi napisanie test i assert zamiast ExUnit.Case.test i ExUnit.Assertions.assert.

Co śmieszne, ExUnit.Case który wymagamy sam siebie importuje ;)

Od teraz w naszym module możemy pisać testy zaczynając od makra test, następnie opisując co test ma robić i implementując test, wiedząc, że mamy dostęp do AppName.method.

Asynchroniczny czy nie

Możemy, jeżeli chcemy uruchamiać testy asynchronicznie lub synchronicznie. To jak uruchomimy testy będzie zależało od naszego kodu. Jeżeli jest on odizolowany i może być testowany jednostkowo wtedy async ma ręce i nogi. Jeżeli jednak zabieramy się za testowanie bardziej integracyjne, dotykające kilku warstw i które polega na jakimś globalnym stanie, to async niekoniecznie musi być dobrym wyjściem.

Konfigurację dotyczącą tego jak testy są wykonywane podajemy w trakcie użycia ExUnit.Case:

use ExUnit.Case, asyc: true|false

Domyślnie, async ustawiany jest na false.

Sprawdzanie wyniku

To jest całkowicie opcjonalne. Jeżeli chcemy mieć więcej informacji, to możemy się stosować do makr tutaj opisanych. Jeżeli jednak nas to nie interesuje, to wystarczy, ze nasz kod będzie zwracał jakiś błąd. Na przykład match error:

test "test" do
    2 = 3
end

To do dostaniemy to:

1) test the truth (BencodeTest)
    test/bencode_test.exs:5
    ** (MatchError) no match of right hand side value: 3
    stacktrace:
      test/bencode_test.exs:6: (test)

Zamiast:

1) test the truth (BencodeTest)
    test/bencode_test.exs:5
    Assertion with == failed
    code: 2 == 3
    lhs:  2
    rhs:  3
    stacktrace:
      test/bencode_test.exs:6: (test)

assert i refute

Mamy dwie możliwości z assert. Możemy założyć, że coś ma daną wartość:

1 + 1 == 2

Lub możemy założyć, że coś nie ma takiej wartości

1 + 1 == 3

W pierwszym wypadku korzystamy z assert, w drugim refute.

assert_raise

Podobnie też jak w językach obiektowych, możemy założyć, że coś wywali błąd: assert_raise który przyjmuje typ błędu a potem lambdę która ma ten błąd wywołać:

assert_raise ArithmeticError, fn ->
  1 + "test"
end

assert_received i assert_receive

Makra przydają się kiedy chcemy sprawdzić czy proces otrzymał wiadomość wysłaną za pomocą send. Na wykonanie metody się nie czeka (received) albo czeka (receive). Makra weryfikują, czy wiadomość znajduje się w skrzynce odbiorczej procesu. Jeżeli nie, to dostaniemy wyjątek.

test "receives ping" do
    send self(), :ping
    assert_received :ping
end

inne

Jest jeszcze parę opcji, jak na przykład catch_error czy catch_exit ale z nich nie korzystałem i nawet nie wiem zbytnio co i jak. Ogólnie to tak jakby oczekiwać, że będzie błąd i zwrócić ten błąd. Nie wiem zbytnio czemu miałbym użyć to a nie assert_raise. Chyba że diabeł tkwi w szczegółach.

Co to jest doctest?

doctest to makro, które działa podobnie jak use, z tym wyjątkiem, że zamiast wywoływać metodę __using__ robi to dla metody __doctests__ która jest odpowiedzialna za:

  1. Wczytanie wszystkich komentarzy w danym module
  2. Przefiltrowanie komentarzy po parametrach doctest (możemy określić jakie metody mają działać :only, lub które mają nie być brane pod uwagę :except)
  3. Wygenerowanie kodu w locie którego zawartość testu jest równa przykładowi, a porównanie naszemu wynikowi

Dosłownie doctest wykorzystuje to, że elixir jest meta językiem i w locie tworzy nam kod, który ma zostać przekompilowany i uruchomiony.

Nie ma tutaj więc magii, ładowany jest nasz plik modułu, analizowany i na końcu wypluta jest konwersja.

Podsumowanie

Na tym zakończymy część pierwszą. W drugiej zajmiemy się parametrami, grupowaniem i może już też pisaniem testów, jak nie to zrobimy to w trzeciej. Jak się okazuje, tego jest tyle, że by to ogarnąć i napisać rozsądnie a nie po łebkach to musiałem się zatrzymać.

Szczegółów doctest – implementacyjnych – nie omawiam, bo to będzie dobry pomysł by zrobić osobny post może dwa jak coś takiego sami możemy zrobić. Bardzo fajna opcja.

Do przeczytania za tydzień!