Jeden czy wiele plików do testowania jednostkowego jednej klasy?

20

Badając najlepsze praktyki testowania jednostek, aby pomóc w opracowaniu wytycznych dla mojej organizacji, natknąłem się na pytanie, czy lepiej lub przydatniej jest oddzielić urządzenia testowe (klasy testowe) czy zachować wszystkie testy dla jednej klasy w jednym pliku.

Fwiw, mam na myśli „testy jednostkowe” w czystym sensie, że są to testy białych skrzynek ukierunkowane na jedną klasę, jedno stwierdzenie na test, wyśmiewane wszystkie zależności itp.

Przykładowym scenariuszem jest klasa (nazwij to Dokument), która ma dwie metody: CheckIn i CheckOut. Każda metoda implementuje różne reguły itp., Które kontrolują ich zachowanie. Zgodnie z regułą jednego potwierdzenia na test, będę miał wiele testów dla każdej metody. Mogę umieścić wszystkie testy w jednej DocumentTestsklasie o nazwach takich jak CheckInShouldThrowExceptionWhenUserIsUnauthorizedi CheckOutShouldThrowExceptionWhenUserIsUnauthorized.

Mogę też mieć dwie osobne klasy testowe: CheckInShouldi CheckOutShould. W takim przypadku moje nazwy testów zostałyby skrócone, ale byłyby zorganizowane, aby wszystkie testy dla określonego zachowania (metody) były razem.

Jestem pewien, że są plusy i minusy, aby podejść i zastanawiam się, czy ktoś poszedł tą drogą z wieloma plikami, a jeśli tak, to dlaczego? Lub, jeśli wybrałeś podejście z jednym plikiem, dlaczego uważasz, że jest to lepsze?

SonOfPirate
źródło
2
Nazwy metod testowych są za długie. Uprość je, nawet jeśli oznacza to, że metoda testowa zawierałaby wiele twierdzeń.
Bernard
12
@Bernard: za niewłaściwą praktykę uważa się wiele twierdzeń na metodę, jednak długie nazwy metod testowych NIE są uważane za złą praktykę. Zazwyczaj dokumentujemy, co metoda testuje w samej nazwie. Np. Constructor_nullSomeList (), setUserName_UserNameIsNotValid () itp. ...
c_maker
4
@Bernard: Używam też długich nazw testowych (i tylko tam!). Uwielbiam je, bo jest tak jasne, co nie działało (jeśli twoje imiona są dobrym wyborem;)).
Sebastian Bauer,
Wiele twierdzeń nie jest złą praktyką, jeśli wszystkie testują ten sam wynik. Na przykład. nie miałby testResponseContainsSuccessTrue(), testResponseContainsMyData()a testResponseStatusCodeIsOk(). Można by mieć je w jednym testResponse(), który trzy dochodzi: assertEquals(200, response.status), assertEquals({"data": "mydata"}, response.data)iassertEquals(true, response.success)
Juha Untinen

Odpowiedzi:

18

Jest to rzadkie, ale czasem sensowne jest posiadanie wielu klas testowych dla danej klasy testowanej. Zazwyczaj robiłbym to, gdy wymagane są różne konfiguracje i dzielone w ramach podzbioru testów.

Carl Manaster
źródło
3
Dobry przykład, dlaczego to podejście zostało mi przedstawione. Na przykład, gdy chcę przetestować metodę CheckIn, zawsze chcę, aby testowany obiekt był konfigurowany w jedną stronę, ale kiedy testuję metodę CheckOut, wymagam innej konfiguracji. Słuszna uwaga.
SonOfPirate,
14

Naprawdę nie widzę żadnego istotnego powodu, dla którego miałbyś podzielić test dla jednej klasy na wiele klas testowych. Ponieważ głównym celem powinno być utrzymanie spójności na poziomie klasy, powinieneś dążyć do tego również na poziomie testowym. Kilka przypadkowych powodów:

  1. Nie trzeba powielać (i utrzymywać wielu wersji) kodu konfiguracji
  2. Łatwiej uruchomić wszystkie testy dla klasy z IDE, jeśli są one zgrupowane w jednej klasie testowej
  3. Łatwiejsze rozwiązywanie problemów dzięki mapowaniu jeden na jeden między klasami testowymi i klasami.
papka
źródło
Słuszne uwagi. Jeśli testowana klasa jest okropną kludge, nie wykluczałbym kilku klas testowych, w których niektóre zawierają logikę pomocniczą. Po prostu nie lubię bardzo sztywnych zasad. Poza tym świetne punkty.
Job
1
Wyeliminowałbym zduplikowany kod instalacyjny, mając wspólną klasę bazową dla urządzeń testowych pracujących z jedną klasą. Nie jestem pewien, czy w ogóle zgadzam się z pozostałymi dwoma punktami.
SonOfPirate,
W JUnit np. Możesz chcieć testować rzeczy przy użyciu różnych Runnerów, co prowadzi do potrzeby różnych klas.
Joachim Nilsson
Gdy piszę klasę z dużą liczbą metod o złożonej matematyce w każdej z nich, stwierdzam, że mam 85 testów i jak dotąd napisałem testy tylko dla 24% metod. Znalazłem się tutaj, ponieważ zastanawiałem się, czy to szalone, aby jakoś rozdzielić tę ogromną liczbę testów na osobne kategorie, aby móc uruchamiać podzbiory.
Mooing Duck,
7

Jeśli jesteś zmuszony podzielić testy jednostkowe dla klasy na wiele plików, może to wskazywać, że sama klasa jest źle zaprojektowana. Nie mogę wymyślić żadnego scenariusza, w którym bardziej korzystne byłoby podzielenie testów jednostkowych dla klasy, która w rozsądny sposób przestrzega zasady pojedynczej odpowiedzialności i innych dobrych praktyk programistycznych.

Co więcej, dłuższe nazwy metod w testach jednostkowych są dopuszczalne, ale jeśli ci to przeszkadza, zawsze możesz przemyśleć konwencję nazewnictwa testów jednostkowych, aby skrócić nazwy.

FishBasketGordo
źródło
Niestety, w prawdziwym świecie nie wszystkie klasy stosują się do SRP i in. ... i staje się to problemem, gdy testujesz jednostkę starszego kodu.
Péter Török,
3
Nie jestem pewien, jak odnosi się tutaj SRP. W swojej klasie mam wiele metod i muszę napisać testy jednostkowe dla wszystkich różnych wymagań, które determinują ich zachowanie. Czy lepiej jest mieć jedną klasę testową z 50 metodami testowymi lub 5 klas testowych z 10 testami, z których każda dotyczy określonej metody w badanej klasie?
SonOfPirate,
2
@ SonOfPirate: Jeśli potrzebujesz tak wielu testów, może to oznaczać, że twoje metody robią za dużo. Tak więc SRP ma tutaj duże znaczenie. Jeśli zastosujesz SRP do klas, a także metod, będziesz mieć wiele małych klas i metod, które są łatwe do przetestowania i nie będziesz potrzebował tylu metod testowych. (Ale zgadzam się z Péterem Törökiem, że podczas testowania starszego kodu dobre praktyki są na wyciągnięcie
ręki
Najlepszym przykładem, jaki mogę podać, jest metoda CheckIn, w której mamy reguły biznesowe, które określają, kto może wykonać akcję (autoryzację), jeśli obiekt jest w odpowiednim stanie do wpisania, np. Czy jest wyewidencjonowany i czy zmiany zostało zrobione. Będziemy przeprowadzać testy jednostkowe weryfikujące egzekwowanie reguł autoryzacji, a także testy sprawdzające, czy metoda zachowuje się poprawnie, jeśli funkcja CheckIn zostanie wywołana, gdy obiekt nie zostanie wyewidencjonowany, nie zostanie zmieniony itp. Dlatego otrzymujemy wiele testy. Sugerujesz, że to źle?
SonOfPirate,
Nie sądzę, aby na ten temat były trudne i szybkie, właściwe lub złe odpowiedzi. Jeśli to, co robisz, działa dla ciebie, to świetnie. To tylko moja opinia na temat ogólnych wytycznych.
FishBasketGordo,
2

Jednym z moich argumentów przeciwko podzieleniu testów na wiele klas jest to, że trudniej jest innym programistom w zespole (szczególnie tym, którzy nie są tak doświadczeni w testowaniu) zlokalizowanie istniejących testów ( Gee, zastanawiam się, czy jest już test na to metoda? Zastanawiam się, gdzie by to było? ), a także gdzie umieścić nowe testy ( zamierzam napisać test dla tej metody w tej klasie, ale nie jestem pewien, czy powinienem umieścić je w nowym pliku, czy w istniejącym jeden? )

W przypadku, gdy różne testy wymagają drastycznie różnych ustawień, widziałem, jak niektórzy praktykujący TDD umieszczają „urządzenie” lub „konfigurację” w różnych klasach / plikach, ale nie same testy.

rajd25rs
źródło
1

Chciałbym móc zapamiętać / znaleźć link, w którym technika, którą wybrałem, została po raz pierwszy zademonstrowana. Zasadniczo tworzę jedną, abstrakcyjną klasę dla każdej testowanej klasy, która zawiera zagnieżdżone urządzenia testowe (klasy) dla każdego testowanego elementu. Zapewnia to pierwotnie pożądaną separację, ale zachowuje wszystkie testy w tym samym pliku dla każdej lokalizacji. Ponadto generuje to nazwę klasy, która umożliwia łatwe grupowanie i sortowanie w module testowym.

Oto przykład zastosowania tego podejścia do mojego oryginalnego scenariusza:

public abstract class DocumentTests : TestBase
{
    [TestClass()]
    public sealed class CheckInShould : DocumentTests
    {
        [TestMethod()]
        public void ThrowExceptionWhenUserIsNotAuthorized()
        {
        }
    }

    [TestClass()]
    public sealed class CheckOutShould : DocumentTests
    {
        [TestMethod()]
        public void ThrowExceptionWhenUserIsNotAuthorized()
        {
        }
    }
}

Powoduje to pojawienie się następujących testów na liście testów:

DocumentTests+CheckInShould.ThrowExceptionWhenUserIsNotAuthorized
DocumentTests+CheckOutShould.ThrowExceptionWhenUserIsNotAuthorized

W miarę dodawania kolejnych testów można je łatwo grupować i sortować według nazw klas, co powoduje, że wszystkie testy dla badanej klasy są wymienione razem.

Kolejną zaletą tego podejścia, które nauczyłem się wykorzystywać, jest zorientowany obiektowo charakter tej struktury, co oznacza, że ​​mogę zdefiniować kod w metodach uruchamiania i czyszczenia klasy bazowej DocumentTests, które będą współużytkowane przez wszystkie klasy zagnieżdżone, a także wewnątrz każdej zagnieżdżona klasa dla zawartych w niej testów.

SonOfPirate
źródło
1

Spróbuj pomyśleć przez odwrót.

Dlaczego miałbyś mieć tylko jedną klasę testową na klasę? Czy przeprowadzasz test klasowy czy test jednostkowy? Czy w ogóle zależysz na zajęciach ?

Test jednostkowy powinien testować określone zachowanie w określonym kontekście. Fakt, że twoja klasa już ma CheckInśrodki, powinien zapewnić zachowanie, które tego potrzebuje.

Jak byś pomyślał o tym pseudokodzie:

// check_in_test.file

class CheckInTest extends TestCase {
        /** @test */
        public function unauthorized_users_cannot_check_in() {
                $this->expectException();

                $document = new Document($unauthorizedUser);

                $document->checkIn();
        }
}

Teraz nie testujesz bezpośrednio tej checkInmetody. Zamiast tego testujesz zachowanie (które jest na szczęście do sprawdzenia;)), a jeśli potrzebujesz przefakturować Documenti podzielić je na różne klasy lub scalić inną klasę, pliki testowe są nadal spójne, nadal mają sens, ponieważ nigdy nie zmieniasz logiki podczas refaktoryzacji, tylko struktura.

Jeden plik testowy dla jednej klasy po prostu utrudnia refaktoryzację, gdy jest to potrzebne, a testy mają również mniej sensu pod względem domeny / logiki samego kodu.

Steve Chamaillard
źródło
0

Zasadniczo zalecałbym myślenie o testach jako o testowaniu zachowania klasy, a nie o metodzie. Tzn. Niektóre z twoich testów mogą wymagać wywołania obu metod w klasie w celu przetestowania niektórych oczekiwanych zachowań.

Zwykle zaczynam od jednej klasy testów jednostkowych na klasę produkcyjną, ale ostatecznie mogę podzielić tę klasę testów jednostkowych na kilka klas testowych w oparciu o testowane zachowanie. Innymi słowy, odradzałbym dzielenie klasy testowej na CheckInShould i CheckOutShould, a raczej dzielenie według zachowania testowanej jednostki.

Christian Horsdal
źródło
0

1. Zastanów się, co może pójść nie tak

Podczas początkowego rozwoju dość dobrze wiesz, co robisz, i oba rozwiązania prawdopodobnie będą w porządku.

Staje się bardziej interesujący, gdy test kończy się niepowodzeniem po zmianie znacznie później. Istnieją dwie możliwości:

  1. Poważnie złamałeś CheckIn(lub CheckOut). Ponownie, zarówno rozwiązanie dla jednego pliku, jak i dla dwóch plików jest w tym przypadku OK.
  2. Zmodyfikowałeś oba CheckIni CheckOut(i być może ich testy) w sposób, który ma sens dla każdego z nich, ale nie dla obu razem. Złamałeś spójność pary. W takim przypadku podzielenie testów na dwa pliki utrudni zrozumienie problemu.

2. Zastanów się, do jakich testów są używane

Testy służą dwóm głównym celom:

  1. Automatycznie sprawdź, czy program nadal działa poprawnie. To, czy testy znajdują się w jednym pliku, czy w kilku, nie ma znaczenia.
  2. Pomóż czytelnikowi zrozumieć program. Będzie to o wiele łatwiejsze, jeśli testy ściśle powiązanych funkcji będą przechowywane razem, ponieważ dostrzeżenie ich spójności jest szybką drogą do zrozumienia.

Więc?

Obie te perspektywy sugerują, że wspólne testowanie może pomóc, ale nie zaszkodzi.

Lutz Prechelt
źródło