Jak napisać test dla czystej metody, która nic nie zwraca?

13

Mam kilka klas, które zajmują się walidacją wartości. Na przykład RangeValidatorklasa sprawdza, czy wartość mieści się w określonym zakresie.

Każda klasa walidatora zawiera dwie metody: is_valid(value)która zwraca wartość Truelub Falsezależy od niej i ensure_valid(value)która sprawdza określoną wartość i albo nie robi nic, jeśli wartość jest poprawna, albo zgłasza określony wyjątek, jeśli wartość nie pasuje do wstępnie zdefiniowanych reguł.

Obecnie z tą metodą związane są dwa testy jednostkowe:

  • Ten, który przekazuje niepoprawną wartość i zapewnia, że ​​wyjątek został zgłoszony.

    def test_outside_range(self):
        with self.assertRaises(demo.ValidationException):
            demo.RangeValidator(0, 100).ensure_valid(-5)
    
  • Ten, który przekazuje prawidłową wartość.

    def test_in_range(self):
        demo.RangeValidator(0, 100).ensure_valid(25)
    

Chociaż drugi test wykonuje swoją pracę - nie powiedzie się, jeśli zostanie zgłoszony wyjątek, a powiedzie się, jeśli ensure_validniczego nie wyrzuci - fakt, że w środku nie ma żadnych assertznaków, wygląda dziwnie. Ktoś, kto czyta taki kod, od razu zadaje sobie pytanie, dlaczego istnieje test, który wydaje się nic nie robić.

Czy jest to obecna praktyka podczas testowania metod, które nie zwracają wartości i nie wywołują skutków ubocznych? Czy powinienem przepisać test w inny sposób? Lub po prostu wstaw komentarz wyjaśniający, co robię?

Arseni Mourzenko
źródło
11
Pedantyczny punkt, ale jeśli masz funkcję, która nie przyjmuje argumentów (z wyjątkiem selfreferencji) i nie zwraca żadnego wyniku, nie jest to funkcja czysta.
David Arno
10
@DavidArno: To nie jest pedantyczna kwestia, trafia do sedna pytania: metoda jest trudna do przetestowania właśnie dlatego, że jest nieczysta.
Jörg W Mittag
@DavidArno Bardziej pedantyczny punkt, możesz mieć metodę „nic nie rób” (zakładając, że „nie zwraca żadnych wyników” jest interpretowane jako „zwraca typ jednostki”, który jest wywoływany voidw wielu językach i ma głupie reguły). Alternatywnie, możesz mieć nieskończoną pętla (która działa nawet wtedy, gdy „nie zwraca wyniku” naprawdę oznacza, że nie zwraca wyników.
Derek Elkins opuścił SE
Istnieje jedna czysta funkcja typu unit -> unitw dowolnym języku, która uważa jednostkę za standardowy typ danych zamiast czegoś magicznie specjalnego. Niestety po prostu zwraca jednostkę i nic więcej nie robi.
Phoshi

Odpowiedzi:

21

Większość frameworków testowych ma wyraźne stwierdzenie „nie rzuca”, np. Jasmine ma, expect(() => {}).not.toThrow();a nUnit i przyjaciele też je mają.

DeadMG
źródło
1
A jeśli środowisko testowe nie ma takiego potwierdzenia, zawsze można utworzyć metodę lokalną, która robi dokładnie to samo, dzięki czemu kod nie wymaga wyjaśnień. Dobry pomysł.
Arseni Mourzenko
7

Zależy to silnie od używanego języka i frameworka. Mówiąc w kategoriach NUnit, istnieją Assert.Throws(...)metody. Możesz przekazać im metodę lambda:

Assert.Throws(() => rangeValidator.EnsureValid(-5))

który jest wykonywany w ramach Assert.Throws. Wywołanie do lambda najprawdopodobniej zostanie otoczone try { } catch { }blokiem, a potwierdzenie nie powiedzie się, jeśli zostanie wychwycony wyjątek.

Jeśli twój framework nie zapewnia tych środków, możesz to obejść, samodzielnie pakując wywołanie (piszę w C #):

// assert that the method does not fail
try
{
    rangeValidator.EnsureValid(50);
}
catch(Exception e)
{
    Assert.IsTrue(false, $"Exception: {e.Message}");
}

// assert that the method does fail
try
{
    rangeValidator.EnsureValid(50);
    Assert.IsTrue(false, $"Method is expected to throw an exception");
}
catch(Exception e)
{
}

To sprawia, że ​​intencja jest bardziej wyraźna, ale do pewnego stopnia zaśmieca kod. (Oczywiście możesz zawinąć wszystkie te rzeczy w metodę). Na końcu to zależy od ciebie.

Edytować

Jak zauważył Doc Brown w komentarzach, problemem nie było wskazanie, że metoda rzuca, ale że nie rzuca. W NUnit jest na to również stwierdzenie

Assert.DoesNotThrow(() => rangeValidator.EnsureValid(-5))
Paul Kertscher
źródło
1

Wystarczy dodać komentarz, aby wyjaśnić, dlaczego nie jest potrzebne żadne potwierdzenie i dlaczego go nie zapomniałeś.

Jak widać w innych odpowiedziach, wszystko inne sprawia, że ​​kod jest bardziej skomplikowany i zaśmiecony. Dzięki komentarzowi inni programiści znają zamiar testu.

Biorąc to pod uwagę, ten rodzaj testu powinien być wyjątkiem (nie ma zamiaru używać słów). Jeśli regularnie piszesz coś takiego, prawdopodobnie testy mówią ci, że projekt nie jest optymalny.

jhyot
źródło
1

Można również stwierdzić, że niektóre metody są wywoływane (lub nie są wywoływane) poprawnie.

Na przykład:

public void SendEmail(User user)
     ... construct email ...
     _emailSender.Send(email);

W teście:

emailSenderMock.VerifyIgnoreArgs(service =>
    service.Send(It.IsAny<Email>()),
    Times.Once
);
Gabriel Robert
źródło