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ę.
źródło
Odpowiedzi:
Napraw kod, aby był lepiej zaprojektowany. Jeśli w testach występują takie problemy, kod będzie miał gorsze problemy podczas próby zmiany.
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.
źródło
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.
źródło
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.
źródło