Test jednostkowy, aby przetestować tworzenie obiektu domeny

11

Mam test jednostkowy, który wygląda następująco:

[Test]
public void Should_create_person()
{
     Assert.DoesNotThrow(() => new Person(Guid.NewGuid(), new DateTime(1972, 01, 01));
}

Zapewniam, że tutaj utworzono obiekt Person, tzn. Że sprawdzanie poprawności nie kończy się niepowodzeniem. Na przykład, jeśli Guid ma wartość zerową lub data urodzenia jest wcześniejsza niż 01.01.1900, wówczas sprawdzanie poprawności zakończy się niepowodzeniem i zostanie zgłoszony wyjątek (co oznacza, że ​​test się nie powiedzie).

Konstruktor wygląda następująco:

public Person(Id id, DateTime dateOfBirth) :
        base(id)
    {
        if (dateOfBirth == null)
            throw new ArgumentNullException("Date of Birth");
        elseif (dateOfBith < new DateTime(1900,01,01)
            throw new ArgumentException("Date of Birth");
        DateOfBirth = dateOfBirth;
    }

Czy to dobry pomysł na test?

Uwaga : kieruję się klasycystycznym podejściem do testów jednostkowych modelu domeny, jeśli ma to jakiś wpływ.

w0051977
źródło
Czy konstruktor ma jakąkolwiek logikę, którą warto potwierdzić po inicjalizacji?
Laiv
2
Nigdy nie zawracaj sobie głowy testowaniem konstruktorów !!! Konstrukcja powinna być prosta. Czy spodziewasz się niepowodzenia w Guid.NewGuid () lub konstruktorze DateTime?
ivenxu
@Laiv, zobacz aktualizację pytania.
w0051977
1
Zaimplementowanie testu jako tego, który udostępniłeś, nie jest nic warte. Testowałbym jednak również odwrotnie. Testowałbym przypadek, w którym data urodzenia powoduje błąd. To niezmiennik klasy, którą chcesz mieć pod kontrolą i testem.
Laiv
3
Test wygląda dobrze, z wyjątkiem jednego: nazwy. Should_create_person? Co powinno stworzyć osobę? Nadaj mu sensowną nazwę, na przykład Creating_person_with_valid_data_succeeds.
David Arno

Odpowiedzi:

18

Jest to poprawny test (choć raczej nadgorliwy) i czasami robię to, aby przetestować logikę konstruktora, jednak jak wspomniał Laiv w komentarzach, powinieneś zadać sobie pytanie, dlaczego.

Jeśli twój konstruktor wygląda tak:

public Person(Guid guid, DateTime dob)
{
  this.Guid = guid;
  this.Dob = dob;
}

Czy warto sprawdzać, czy rzuca? Czy parametry są poprawnie przypisane, rozumiem, ale twój test jest raczej przesadny.

Jeśli jednak twój test wykonuje coś takiego:

public Person(Guid guid, DateTime dob)
{
  if(guid == default(Guid)) throw new ArgumentException("Guid is invalid");
  if(dob == default(DateTime)) throw new ArgumentException("Dob is invalid");

  this.Guid = guid;
  this.Dob = dob;
}

Następnie twój test staje się bardziej odpowiedni (ponieważ w rzeczywistości rzucasz wyjątki gdzieś w kodzie).

Powiem jedno, ogólnie rzecz biorąc, złą praktyką jest posiadanie dużej logiki w konstruktorze. Podstawowe sprawdzanie poprawności (takie jak kontrole zerowe / domyślne, które wykonuję powyżej) jest w porządku. Ale jeśli łączysz się z bazami danych i ładujesz czyjeś dane, wtedy kod zaczyna naprawdę wąchać ...

Z tego powodu, jeśli twój konstruktor jest wart przetestowania (ponieważ jest dużo logiki), to może coś innego jest nie tak.

Prawie na pewno będziesz mieć inne testy obejmujące tę klasę w warstwach logiki biznesowej, konstruktory i przypisania zmiennych prawie na pewno uzyskają pełne pokrycie z tych testów. Dlatego może nie ma sensu dodawanie konkretnych testów specjalnie dla konstruktora. Jednak nic nie jest czarno-białe i nie miałbym nic przeciwko tym testom, gdybym je sprawdzał - ale zastanawiałbym się, czy dodają one dużej wartości ponad testy poza gdzie indziej w twoim rozwiązaniu.

W twoim przykładzie:

public Person(Id id, DateTime dateOfBirth) :
        base(id)
    {
        if (dateOfBirth == null)
            throw new ArgumentNullException("Date of Birth");
        elseif (dateOfBith < new DateTime(1900,01,01)
            throw new ArgumentException("Date of Birth");
        DateOfBirth = dateOfBirth;
    }

Nie tylko sprawdzasz poprawność, ale również wywołujesz konstruktora podstawowego. Dla mnie daje to więcej powodów do przeprowadzania tych testów, ponieważ logika konstruktora / sprawdzania poprawności jest teraz podzielona na dwie klasy, co zmniejsza widoczność i zwiększa ryzyko nieoczekiwanej zmiany.

TLDR

Testy te mają pewną wartość, jednak logika walidacji / przypisania prawdopodobnie będzie objęta innymi testami w twoim rozwiązaniu. Jeśli w tych konstruktorach jest dużo logiki, która wymaga znacznych testów, to sugeruje mi, że czai się tam nieprzyjemny zapach kodu.

Liath
źródło
@Laith, proszę zobaczyć aktualizację mojego pytania
w0051977,
Zauważam, że w swoim przykładzie nazywasz konstruktorem bazy. IMHO zwiększa to wartość twojego testu, logika konstruktora jest teraz podzielona na dwie klasy i dlatego jest nieco wyższe ryzyko zmiany, co daje więcej powodów do przetestowania go.
Liath,
„Jednak jeśli twój test robi coś takiego:„ <Czy nie masz na myśli „jeśli twój konstruktor robi coś takiego” ?
Kodos Johnson
„Testy te mają pewną wartość” - co ciekawe, dla mnie wartość ta pokazuje, że możemy uczynić ten test zbędnym, używając nowej klasy reprezentującej dobro osoby (np. PersonBirthdate), Która dokonuje daty walidacji urodzenia. Podobnie Guidsprawdzenie może zostać zaimplementowane w Idklasie. Oznacza to, że tak naprawdę nie musisz już mieć tej logiki sprawdzania poprawności w Personkonstruktorze, ponieważ nie jest możliwe zbudowanie takiej z niepoprawnymi danymi - z wyjątkiem nullreferencji. Oczywiście musisz napisać testy dla pozostałych dwóch klas :)
Stephen Byrne
12

Już dobra odpowiedź tutaj, ale myślę, że warto wspomnieć o jeszcze jednej rzeczy.

Robiąc TDD „według książki”, należy najpierw napisać test, który wywołuje konstruktor, nawet zanim ten konstrukt zostanie zaimplementowany. Test ten może wyglądać tak jak ten, który przedstawiłeś, nawet jeśli w implementacji konstruktora nie byłoby logiki zerowej weryfikacji.

Zauważ też, że dla TDD najpierw należy napisać inny test

  Assert.Throws<ArgumentException>(() => new Person(Guid.NewGuid(), 
        new DateTime(1572, 01, 01));

przed dodaniem czeku DateTime(1900,01,01)do konstruktora.

W kontekście TDD pokazany test ma całkowicie sens.

Doktor Brown
źródło
Niezły kąt, którego nie wziąłem pod uwagę!
Liath
1
To pokazuje mi, dlaczego tak sztywna forma TDD jest stratą czasu: test powinien mieć wartość po napisaniu kodu lub po prostu piszesz każdą linię kodu dwa razy, raz jako twierdzenie, a raz jako kod. Twierdziłbym, że sam konstruktor nie jest logiką wymagającą testowania; reguła biznesowa „osoby urodzone przed 1900 rokiem nie mogą być reprezentowalne” jest możliwa do przetestowania, a konstruktor jest tam, gdzie ta reguła jest wdrażana, ale kiedy test pustego konstruktora kiedykolwiek wnosi wartość dodaną do projektu?
IMSoP
Czy to naprawdę tdd według książki? Chciałbym utworzyć instancję i wywołać jej metodę od razu w kodzie. Następnie napisałbym test dla tej metody i robiąc to musiałbym również utworzyć instancję dla tej metody, aby zarówno konstruktor, jak i metoda zostały objęte tym testem. Chyba że w konstruktorze jest jakaś logika, ale ta część jest objęta Liath.
Rafał Łużyński
@ RafałŁużyński: TDD „przy książce” polega przede wszystkim na pisaniu testów . W rzeczywistości oznacza to, że zawsze najpierw należy napisać test zakończony niepowodzeniem (brak kompilacji również liczy się jako niepowodzenie). Więc najpierw piszesz test wywołujący konstruktor, nawet gdy nie ma konstruktora . Następnie próbujesz skompilować (co się nie powiedzie), a następnie zaimplementować pusty konstruktor, skompilować, uruchomić test, wynik = zielony. Następnie piszesz pierwszy test zakończony niepowodzeniem i uruchamiasz go - wynik = czerwony, następnie dodajesz funkcjonalność, aby test był ponownie „zielony” i tak dalej.
Doc Brown
Oczywiście. Nie miałem na myśli, że najpierw piszę implementację, a potem test. Po prostu piszę „użycie” tego kodu na wyższym poziomie, następnie testuję ten kod, a następnie go implementuję. Zazwyczaj robię „Outside TDD”.
Rafał Łużyński