Kruche testy jednostkowe z powodu potrzeby nadmiernego kpienia


21

Walczę z coraz bardziej irytującym problemem dotyczącym naszych testów jednostkowych, które wdrażamy w moim zespole. Próbujemy dodać testy jednostkowe do starszego kodu, który nie został dobrze zaprojektowany i chociaż nie mieliśmy żadnych trudności z faktycznym dodaniem testów, zaczynamy zmagać się z tym, jak przebiegają testy.

Jako przykład problemu załóżmy, że masz metodę, która wywołuje 5 innych metod w ramach jej wykonania. Testem dla tej metody może być potwierdzenie, że zachowanie występuje w wyniku wywołania jednej z tych 5 innych metod. Ponieważ test jednostkowy powinien zakończyć się niepowodzeniem tylko z jednego powodu i tylko z jednego powodu, chcesz wyeliminować potencjalne problemy wywołane tymi 4 innymi metodami i wyśmiewać je. Świetny! Test jednostkowy jest wykonywany, wyśmiewane metody są ignorowane (a ich zachowanie można potwierdzić w ramach innych testów jednostkowych), a weryfikacja działa.

Ale pojawia się nowy problem - test jednostkowy ma dogłębną wiedzę na temat tego, w jaki sposób potwierdziłeś, że zachowanie i wszelkie zmiany sygnatur w dowolnej z pozostałych 4 metod w przyszłości, lub wszelkie nowe metody, które należy dodać do „metody nadrzędnej”, będą skutkuje koniecznością zmiany testu jednostkowego, aby uniknąć możliwych awarii.

Oczywiście problem można nieco złagodzić, po prostu dzięki temu, że więcej metod pozwala osiągnąć mniej zachowań, ale miałem nadzieję, że może być dostępne bardziej eleganckie rozwiązanie.

Oto przykładowy test jednostkowy, który wychwytuje problem.

W skrócie „MergeTests” to klasa testów jednostkowych, która dziedziczy z klasy, którą testujemy i zastępuje zachowanie w razie potrzeby. Jest to „wzorzec”, który stosujemy w naszych testach, aby umożliwić nam zastąpienie wywołań zewnętrznych klas / zależności.

[TestMethod]
public void VerifyMergeStopsSpinner()
{
    var mockViewModel = new Mock<MergeTests> { CallBase = true };
    var mockMergeInfo = new MergeInfo(Mock.Of<IClaim>(), Mock.Of<IClaim>(), It.IsAny<bool>());

    mockViewModel.Setup(m => m.ClaimView).Returns(Mock.Of<IClaimView>);
    mockViewModel.Setup(
        m =>
        m.TryMergeClaims(It.IsAny<Func<bool>>(), It.IsAny<IClaim>(), It.IsAny<IClaim>(), It.IsAny<bool>(),
                         It.IsAny<bool>()));
    mockViewModel.Setup(m => m.GetSourceClaimAndTargetClaimByMergeState(It.IsAny<MergeState>())).Returns(mockMergeInfo);
    mockViewModel.Setup(m => m.SwitchToOverviewTab());
    mockViewModel.Setup(m => m.IncrementSaveRequiredNotification());
    mockViewModel.Setup(m => m.OnValidateAndSaveAll(It.IsAny<object>()));
    mockViewModel.Setup(m => m.ProcessPendingActions(It.IsAny<string>()));

    mockViewModel.Object.OnMerge(It.IsAny<MergeState>());    

    mockViewModel.Verify(mvm => mvm.StopSpinner(), Times.Once());
}

Jak reszta z was sobie z tym poradziła lub czy nie ma świetnego „prostego” sposobu radzenia sobie z tym?

Aktualizacja - doceniam opinie wszystkich. Niestety i nie jest to żadną niespodzianką, nie wydaje się, aby istniało świetne rozwiązanie, wzorzec lub praktyka, które można zastosować w testach jednostkowych, jeśli testowany kod jest słaby. Zaznaczyłem odpowiedź, która najlepiej uchwyciła tę prostą prawdę.


Wow, widzę tylko próbną konfigurację, brak instancji SUT ani nic, testujesz tutaj jakąkolwiek rzeczywistą implementację? Kto powinien zadzwonić do StopSpinner? OnMerge? Powinieneś kpić z wszelkich zależności, które może wywoływać, ale nie z samej rzeczy ..
Joppe

Trochę trudno to zobaczyć, ale Mock <MergeTests> to SUT. Ustawiamy flagę CallBase, aby upewnić się, że metoda „OnMerge” wykonuje się na rzeczywistym obiekcie, ale wyśmiewają metody wywoływane przez „OnMerge”, które mogłyby spowodować niepowodzenie testu z powodu problemów z zależnościami itp. Celem testu jest ostatni wiersz - w celu weryfikacji zatrzymaliśmy w tym przypadku pokrętło.
PremiumTier

MergeTests brzmi jak kolejna klasa instrumentalna, a nie coś, co żyje w produkcji, stąd zamieszanie.
Joppe


1
Zupełnie poza twoimi innymi problemami, wydaje mi się błędem, że twój SUT to fałszywy <MergeTests>. Dlaczego miałbyś testować Mocka? Dlaczego nie testujesz samej klasy MergeTests?
Eric King

Odpowiedzi:


18
  1. Napraw kod, aby był lepiej zaprojektowany. Jeśli w testach występują takie problemy, kod będzie miał gorsze problemy podczas próby zmiany.

  2. Jeśli nie możesz, być może musisz być mniej idealny. Testuj przed i po zastosowaniu metody. Kogo to obchodzi, jeśli używasz pozostałych 5 metod? Prawdopodobnie mają własne testy jednostkowe, które wyjaśniają (er), co spowodowało błąd, gdy testy się nie powiodły.

„testy jednostkowe powinny mieć tylko jeden powód do niepowodzenia” to dobra wskazówka, ale z mojego doświadczenia wynika, że ​​jest niepraktyczna. Trudne do napisania testy nie są pisane. Kruche testy nie są akceptowane.


Zgadzam się całkowicie z poprawieniem projektu kodu, ale w mniej idealnym świecie rozwoju dla dużej firmy z napiętymi terminami trudno jest uzasadnić spłatę długu technicznego zaciągniętego przez poprzednie zespoły lub złe decyzje pewnego razu. Jeśli chodzi o twój drugi punkt, wiele kpin nie jest po prostu dlatego, że chcemy, aby test zakończył się niepowodzeniem tylko z jednego powodu - to dlatego, że wykonywany kod nie może zostać wykonany bez uprzedniej obsługi dużej liczby zależności utworzonych w tym kodzie . Przepraszamy za przeniesienie słupków bramkowych na ten.
PremiumTier

Jeśli lepszy projekt nie jest realistyczny, zgadzam się z „Kogo to obchodzi, jeśli używasz pozostałych 5 metod?” Sprawdź, czy metoda wykonuje wymaganą funkcję, a nie jak to robi.
Kwebble,

@Kwebble - Zrozumiano, jednak celem pytania było ustalenie, czy istnieje prosty sposób zweryfikowania zachowania metody, gdy trzeba wyśmiewać inne zachowania wywoływane w ramach metody, aby w ogóle uruchomić test. Chcę usunąć „jak”, ale nie wiem jak :)
PremiumTier

Nie ma magicznej srebrnej kuli. Nie ma „prostego sposobu” przetestowania słabego kodu. Albo testowany kod musi zostać refaktoryzowany, albo sam kod testowy również będzie słaby. Albo test będzie słaby, ponieważ będzie zbyt specyficzny dla wewnętrznych szczegółów, ponieważ natrafiłeś lub, jak sugeruje btilly , możesz uruchomić testy w środowisku roboczym, ale wtedy testy będą znacznie wolniejsze i bardziej złożone. Tak czy inaczej, testy będą trudniejsze do napisania, trudniejsze do utrzymania i podatne na fałszywe negatywy.
Steven Doggart

8

Podział dużych metod na bardziej ukierunkowane małe metody jest zdecydowanie najlepszą praktyką. Widzisz to jako ból przy weryfikacji zachowania testu jednostkowego, ale odczuwasz ból również na inne sposoby.

To powiedziawszy, jest to herezja, ale osobiście jestem fanem tworzenia realistycznych tymczasowych środowisk testowych. Oznacza to, że zamiast wyśmiewać wszystko, co jest ukryte w tych innych metodach, upewnij się, że istnieje łatwe do skonfigurowania środowisko tymczasowe (wraz z prywatnymi bazami danych i schematami - tutaj może pomóc SQLite), które pozwala uruchomić wszystkie te rzeczy. Odpowiedzialność za umiejętność budowania / usuwania środowiska testowego spoczywa na kodzie, który tego wymaga, aby po jego zmianie nie trzeba było zmieniać całego kodu testu jednostkowego zależnego od jego istnienia.

Ale zauważam, że z mojej strony jest to herezja. Ludzie, którzy są mocno zaangażowani w testowanie jednostkowe, opowiadają się za „czystymi” testami jednostkowymi i nazywają to, co opisałem, „testami integracyjnymi”. Nie martwię się osobiście o to rozróżnienie.


3

Zastanowiłbym się nad złagodzeniem prób i po prostu sformułowałbym testy, które mogą obejmować metody, które wywołuje.

Nie testuj jak , testuj co . Liczy się wynik, w razie potrzeby uwzględnij metody podrzędne.

Z innej strony możesz sformułować test, sprawić, by zdał jedną wielką metodą, refaktoryzować i skończyć z drzewem metod po refaktoryzacji. Nie musisz testować każdego z nich osobno. Liczy się wynik końcowy.

Jeśli metody podrzędne utrudniają przetestowanie niektórych aspektów, rozważ podzielenie ich na osobne klasy, abyś mógł wyśmiewać je bardziej czysto, bez poddawania testowanej klasy silnym instrumentom / łączeniu. Trudno powiedzieć, czy faktycznie testujesz jakąś konkretną implementację w przykładowym teście.


Problem polega na tym, że musimy wyśmiewać „jak”, aby przetestować „co”. Jest to ograniczenie narzucone przez projekt kodu. Z pewnością nie chcę „kpić” z tego powodu, że to właśnie powoduje, że test jest kruchy.
PremiumTier

Patrząc na nazwy metod, myślę, że twoja testowana klasa po prostu przyjmuje zbyt wiele obowiązków. Przeczytaj o zasadzie pojedynczej odpowiedzialności. Pożyczki z MVC mogą trochę pomóc, wydaje się, że Twoja klasa obsługuje zarówno interfejs użytkownika, infrastrukturę, jak i problemy biznesowe.
Joppe

Tak :( To byłby ten źle zaprojektowany starszy kod, o którym mówiłem. Pracujemy nad przeprojektowaniem i refaktoryzacją, ale uważaliśmy, że najlepiej byłoby najpierw przetestować źródło.
PremiumTier
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.