Testowanie wyjątków

Zdarza się, że nasz kod komunikuje błędy za pomocą wyjątków. Nieprawidłowe zachowanie musi zostać przetestowane w taki sam sposób jak prawidłowe. Jak jednak zadbać o przetestowanie wyjątków?

Adnotacje

Pierwszym podejściem, jakie wielu osobom przychodzi do głowy, jest użycie adnotacji dostępnych w jUnit czy w PHPUnit. Możemy za ich pomocą określić, jakiego typu wyjątku się spodziewamy, a także podać nieco więcej szczegółów: kod błędu, wiadomość.

W PHP (PHPUnit) wygląda to następująco:

/**
 * @expectedException AwesomeException
 */
public function testThrowAwesomeException()
{
    throw new AwesomeException();
}

W Javie (jUnit) jest podobnie:

@Test(expected = AwesomeException.class)
public void shouldThrowAwesomeException() throws Exception {
    throw new AwesomeException();
}

W JavaScripcie, o ile mi wiadomo, nie ma odpowiednika (dekoratory są na razie tylko eksperymentalną funkcją dostępną w transpilatorach ES7 i TypeScript).

Takie podejście jest jednak obarczone poważnym problemem. Można je porównać do owinięcia całego kodu testu w wielki blok try, a na koniec dodanie catch, gdzie upewniamy się, że rzucony został wyjątek opisany w adnotacji. Jeśli test ma jedną linijkę, to nie ma problemu, ale jeśli zawiera więcej kodu, to skąd możemy mieć pewność, że wyjątek został rzucony dokładnie tam, gdzie się tego spodziewamy? Możemy oczywiście doprecyzować, o jaki wyjątek nam chodzi, np. oczekiwać konkretnego komunikatu błędu, ale to nie rozwiązuje problemu.

Taka sytuacja nie jest może najczęstsza, ale potencjalnie może doprowadzić do sytuacji, w której pomimo poprawnie przechodzącego testu, kod produkcyjny nie działa prawidłowo. Nie możemy oddać całkowicie kontroli nad testowaniem wyjątków narzędziu; musimy sami się tym zająć, co może wymagać nieco więcej kodu, ale w ostatecznym rozrachunku przyniesie nam korzyści.

Bloki try i catch

Nieco lepszym podejściem jest ręczne użycie bloków try i catch — upewniamy się wtedy, że wyjątek został rzucony dokładnie tam, gdzie się go spodziewamy (zakładając, że w bloku try znajdzie się tylko jedna linijka kodu).

PHP:

public function testThrowAwesomeException()
{
    try {
        throw new AwesomeException();
        $this->fail("Expected an exception to be thrown.");
    } catch (AwesomeException $e) {
        // opcjonalne asercje
    }
}

Java:

@Test
public void shouldThrowAwesomeException() throws Exception {
    try {
        throw new AwesomeException();
        fail("Expected an exception to be thrown.");
    } catch (AwesomeException e) {
        // opcjonalne asercje
    }
}

JavaScript (Jasmine):

it("should throw AwesomeError", () => {
    try {
        throw new AwesomeError();
        fail("Expected an error to be thrown.");
    } catch (err) {
        // opcjonalne asercje
    }
});

Zauważmy, że wszystkie trzy przypadki wymagają nieco więcej kodu pisanego ręcznie. Same bloki try i catch są oczywiste, ale w każdym przypadku dochodzi dodatkowo metoda fail() zaraz po wywołaniu testowanego kodu. Jest ona potrzebna w przypadku, gdyby kod jednak nie rzucił wyjątku. Ponieważ wymagamy, aby wyjątek został rzucony, musimy ręcznie zadbać o to, by w razie jego braku, test zakończył się niepowodzeniem.

Asercje wyjątków

Jest też ciekawszy sposób. Zauważmy, że JavaScript (przynajmniej w przypadku Jasmine) robi to tak:

it("should throw AwesomeError", () => {
    expect(() => { throw new AwesomeError(); })
        .toThrowError(AwesomeError);
});

Asercja ma kilka wariantów, umożliwiających nam upewnienie się, że kod nie rzuca błędu, czy też że rzuca błąd z daną wiadomością. Podobne podejście może być osiągnięte w PHP i w Javie, choć wymaga to użycia konkretnych bibliotek. W przypadku PHP znalazłem paczkę o nazwie phpFluentAssertions, która umożliwia zmianę stylu asercji PHPUnit, przy okazji umożliwiając eleganckie testowanie wyjątków:

public function testThrowAwesomeException()
{
    $awesome = function () {
        throw new AwesomeException();
    };

    $this->assert($awesome())
        ->should()->throwException("AwesomeException");
}

Na koniec Java. Jeśli używamy AssertJ w wersji 3, to mamy dostęp do chyba najciekawszej i najbardziej rozwiniętej ze wszystkich prezentowanych opcji. Oprócz dwóch alternatywnych składni, mamy dostęp do asercji, które umieszczamy w płynny sposób w jednym łańcuchu wywołań, dzięki czemu możemy za jednym zamachem sprawdzić wszystko, co nas w danym wyjątku interesuje:

@Test
public void shouldThrowAwesomeException() throws Exception {
    assertThatThrownBy(() -> { throw new AwesomeException("Awesome!"); })
            .isInstanceOf(AwesomeException.class)
            .hasMessageContaining("Awesome!");
}

Napisz komentarz


Szukaj wpisów


Chmura tagów