Zacząłem pisać testy jednostkowe dla mojego obecnego projektu. Ale tak naprawdę nie mam z tym doświadczenia. Najpierw chcę całkowicie go „zdobyć”, więc obecnie nie używam ani mojego środowiska IoC, ani fałszywej biblioteki.
Zastanawiałem się, czy jest coś złego w podawaniu zerowych argumentów konstruktorom obiektów w testach jednostkowych. Pozwól mi podać przykładowy kod:
public class CarRadio
{...}
public class Motor
{
public void SetSpeed(float speed){...}
}
public class Car
{
public Car(CarRadio carRadio, Motor motor){...}
}
public class SpeedLimit
{
public bool IsViolatedBy(Car car){...}
}
Jeszcze inny przykład kodu samochodu (TM), zredukowany do tylko części ważnych dla pytania. Napisałem test coś takiego:
public class SpeedLimitTest
{
public void TestSpeedLimit()
{
Motor motor = new Motor();
motor.SetSpeed(10f);
Car car = new Car(null, motor);
SpeedLimit speedLimit = new SpeedLimit();
Assert.IsTrue(speedLimit.IsViolatedBy(car));
}
}
Test działa dobrze. SpeedLimit
potrzebuje Car
z Motor
celem, co robi. Nie jest wcale zainteresowany CarRadio
, więc podałem dla tego wartość zerową.
Zastanawiam się, czy obiekt zapewniający poprawną funkcjonalność bez pełnej budowy jest naruszeniem SRP lub zapachem kodu. Mam to dokuczliwe wrażenie, że tak jest, ale speedLimit.IsViolatedBy(motor)
też nie jest w porządku - ograniczenie prędkości jest naruszane przez samochód, a nie przez silnik. Może po prostu potrzebuję innej perspektywy dla testów jednostkowych niż działającego kodu, ponieważ cała intencja polega na przetestowaniu tylko części całości.
Czy konstruowanie obiektów z zerowym testem jednostkowym ma zapach kodu?
źródło
Motor
prawdopodobnie nie powinien miećspeed
wcale. Powinien miećthrottle
i obliczyć natorque
podstawie bieżącejrpm
ithrottle
. Zadaniem samochodu jest wykorzystanie goTransmission
do zintegrowania tego z aktualną prędkością i przekształcenie go wrpm
sygnał z powrotem doMotor
... Ale wydaje mi się, że i tak nie byłeś zaangażowany w realizm, prawda?null
radia, ograniczenie prędkości jest poprawnie obliczone. Teraz możesz utworzyć test, aby sprawdzić ograniczenie prędkości za pomocą radia; na wypadek, gdyby zachowanie się różniło ...Odpowiedzi:
W przypadku powyższego przykładu uzasadnione jest, że a
Car
może istnieć bezCarRadio
. W takim przypadku powiedziałbym, że nie tylkoCarRadio
czasami dopuszczalne jest wprowadzenie wartości zerowej , ale także to, że jest to obowiązkowe. Twoje testy muszą zapewnić, że implementacjaCar
klasy jest niezawodna i nie zgłasza wyjątków zerowego wskaźnika, gdy nieCarRadio
występuje.Weźmy jednak inny przykład - rozważmy
SteeringWheel
. Załóżmy, że aCar
musi miećSteeringWheel
, ale test prędkości tak naprawdę nie obchodzi go. W tym przypadku nie dałbym wartości zerowej,SteeringWheel
ponieważ popycha toCar
klasę w miejsca, do których nie jest przeznaczona. W takim przypadku lepiej byłoby stworzyć coś,DefaultSteeringWheel
co (aby kontynuować metaforę) jest zablokowane w linii prostej.źródło
Car
czy nie da się zbudować bezSteeringWheel
...Car
bezCarRadio
, nie ma sensu tworzyć konstruktora, który nie wymaga przekazywania i nie musi martwić się wyraźnym zeriem ?Car
wyrzuciłyby NRE z wartością zerowąCarRadio
, ale odpowiedni kodSpeedLimit
nie. W przypadku zamieszczonego teraz pytania byłoby poprawne i rzeczywiście to zrobiłbym.Tak. Zwróć uwagę na definicję zapachu kodu C2 : „CodeSmell to wskazówka, że coś może być nie tak, a nie pewność”.
Jeśli próbujesz wykazać, że
speedLimit.IsViolatedBy(car)
nie zależy to odCarRadio
argumentu przekazanego konstruktorowi, prawdopodobnie przyszłym opiekunem byłoby wyraźniejsze określenie tego ograniczenia poprzez wprowadzenie podwójnego testu, który nie przejdzie testu, jeśli zostanie wywołany.Jeśli, co jest bardziej prawdopodobne, po prostu próbujesz zaoszczędzić sobie pracy nad stworzeniem, o
CarRadio
którym wiesz, że nie jest używane w tym przypadku, powinieneś zauważyć, żeCarRadio
nieużywany wyciekł do testu)Car
bez określaniaCarRadio
Podejrzewam, że kod próbuje ci powiedzieć, że potrzebujesz konstruktora, który wygląda
a następnie ten konstruktor może zdecydować, co zrobić z faktem, że nie ma
CarRadio
lub
lub
itp.
To drugi interesujący zapach - aby przetestować SpeedLimit, musisz zbudować cały samochód wokół radia, nawet jeśli jedyną rzeczą, na której prawdopodobnie zależy SpeedLimit, jest prędkość .
Może to być wskazówka, która
SpeedLimit.isViolatedBy
powinna akceptować interfejs roli, który implementuje samochód , zamiast wymagać całego samochodu.IHaveSpeed
to naprawdę kiepskie imię; mam nadzieję, że Twój język domeny ulegnie poprawie.Przy takim interfejsie test na
SpeedLimit
może być znacznie prostszyźródło
IHaveSpeed
interfejs, ponieważ (przynajmniej w c #) nie byłbyś w stanie utworzyć samego interfejsu.Nie
Ani trochę. Przeciwnie, jeśli
NULL
wartość jest dostępna jako argument (niektóre języki mogą mieć konstrukcje uniemożliwiające przekazanie wskaźnika zerowego), należy ją przetestować.Kolejne pytanie dotyczy tego, co dzieje się po zdaniu
NULL
. Ale właśnie o to chodzi w teście jednostkowym: upewnienie się, że dla każdego możliwego wejścia dochodzi do określonego wyniku. INULL
jest potencjalne wejście, więc trzeba mieć określony rezultat i trzeba mieć test na to.Więc jeśli twój samochód ma być budowany bez radia, powinieneś napisać test na to. Jeśli nie jest i ma rzucić wyjątek, powinieneś napisać test na to.
Tak czy inaczej, tak, powinieneś napisać test dla
NULL
. Byłby to zapach kodu, gdybyś go pominął. Oznaczałoby to, że masz nieprzetestowaną gałąź kodu, która, jak zakładałeś, będzie magicznie wolna od błędów.Pamiętaj, że zgadzam się z innymi odpowiedziami, że Twój testowany kod może zostać ulepszony. Niemniej jednak, jeśli ulepszony kod ma parametry, które są wskaźnikami, przekazywanie
NULL
nawet dla ulepszonego kodu jest dobrym testem. Chciałbym, aby test pokazał, co się stanie, gdy przejdęNULL
jako silnik. To nie jest zapach kodu, to przeciwieństwo zapachu kodu. To jest testowanie.źródło
Car
tutaj o testowaniu . Chociaż nie widzę niczego, co uważałbym za złe stwierdzenie, pytanie dotyczy testowaniaSpeedLimit
.NULL
konstruktoraCar
.SpeedLimit
. Możesz zobaczyć, że test nazywa się „SpeedLimitTest”, a jedynąAssert
jest sprawdzenie metodySpeedLimit
. TestowanieCar
- na przykład „jeśli Twój samochód ma być budowany bez radia, powinieneś napisać test na to” - odbyłby się w innym teście.Ja osobiście wahałbym się przed wprowadzeniem zer. W rzeczywistości za każdym razem, gdy wstrzykiwam zależności do konstruktora, jedną z pierwszych rzeczy, które robię, jest zgłaszanie wyjątku ArgumentNullException, jeśli te wstrzyknięcia są zerowe. W ten sposób mogę bezpiecznie korzystać z nich w mojej klasie, bez dziesiątek kontroli zerowych. Oto bardzo powszechny wzór. Zwróć uwagę na użycie tylko do odczytu, aby upewnić się, że te zależności ustawione w konstruktorze nie mogą być później ustawione na null (lub cokolwiek innego):
W związku z powyższym może być może przeprowadzę test jednostkowy lub dwa, w których wstrzykiwam wartości zerowe, aby upewnić się, że moje testy zerowe działają zgodnie z oczekiwaniami.
Powiedziawszy to, staje się to problemem, gdy zrobisz dwie rzeczy:
Na przykład masz:
Więcej informacji na temat korzystania z Moq można znaleźć tutaj .
Oczywiście, jeśli chcesz to zrozumieć bez kpin, najpierw możesz to zrobić, o ile korzystasz z interfejsów. Po prostu utwórz manekina CarRadio i Motor w następujący sposób i wstrzyknij je:
źródło
IMotor
miałgetSpeed()
metodę,DummyMotor
musiałby również zwrócić zmodyfikowaną prędkość po udanymsetSpeed
.To nie jest po prostu w porządku, jest to dobrze znana technika przełamywania zależności w celu wprowadzenia starszego kodu do testowej uprzęży. (Zobacz „Efektywna praca ze starszym kodem” autorstwa Michaela Feathersa.)
Czy to pachnie Dobrze...
Testu nie pachnie. Test wykonuje swoją pracę. Deklaruje „ten obiekt może być pusty, a testowany kod nadal działa poprawnie”.
Zapach jest w trakcie realizacji. Deklarując argument konstruktora, deklarujesz „ta klasa potrzebuje tego do prawidłowego funkcjonowania”. Oczywiście to nieprawda. Wszystko, co jest nieprawdą, jest trochę śmierdzące.
źródło
Cofnijmy się do pierwszych zasad.
Będą jednak konstruktory lub metody, które przyjmują inne klasy jako parametry. Sposób ich obsługi różni się w zależności od przypadku, ale konfiguracja powinna być minimalna, ponieważ są one styczne do celu. Mogą istnieć scenariusze, w których podanie parametru NULL jest całkowicie poprawne - na przykład samochód bez radia.
Jestem zakładając tu, że jesteśmy po prostu testuje linię wyścigową zacząć (aby kontynuować temat samochodów), ale może też chcesz przekazać NULL do innych parametrów, aby sprawdzić, czy wszystkie mechanizmy obronne i kod wyjątku przekazanie funkcjonują jako wymagany.
Jak na razie dobrze. Co jednak, jeśli NULL nie jest prawidłową wartością. Cóż, tutaj jesteś w mrocznym świecie kpiny, stuby, manekiny i podróbki .
Jeśli wydaje się, że to wszystko wymaga trochę więcej, to już używasz manekina, przekazując NULL w konstruktorze Car, ponieważ jest to wartość, która potencjalnie nie zostanie użyta.
To, co powinno stać się jasne dość szybko, to to, że potencjalnie strzelasz do kodu z dużą ilością obsługi NULL. Jeśli zamienisz parametry klasy na interfejsy, utorujesz również drogę do wyśmiewania się, DI itp., Które znacznie ułatwiają obsługę.
źródło
Patrząc na twój projekt, sugerowałbym, że a
Car
składa się z zestawuFeatures
. Cechą być możeCarRadio
, lubEntertainmentSystem
który może składać się z rzeczy takie jakCarRadio
,DvdPlayer
etc.Co najmniej musiałbyś zbudować
Car
zestaw zFeatures
.EntertainmentSystem
iCarRadio
byłyby implementatorami interfejsu (Java, C # itp.)public [I|i]nterface Feature
i byłoby to wystąpienie wzorca projektowania złożonego.Dlatego zawsze konstruowałbyś
Car
z,Features
a test jednostkowy nigdy nie utworzyłbyCar
bezFeatures
. Chociaż możesz rozważyć wyśmiewanie go,BasicTestCarFeatureSet
który ma minimalny zestaw funkcji wymaganych do najbardziej podstawowego testu.Tylko kilka myśli.
Jan
źródło