Czy jest to właściwe zastosowanie metody resetowania Mockito?

68

Mam prywatną metodę w mojej klasie testowej, która konstruuje często używany Barobiekt. BarKonstruktor wywołuje someMethod()metodę w moim wyśmiewali obiektu:

private @Mock Foo mockedObject; // My mocked object
...

private Bar getBar() {
  Bar result = new Bar(mockedObject); // this calls mockedObject.someMethod()
}

W niektórych moich metodach testowych, które chcę sprawdzić, someMethodzostał również wywołany przez ten konkretny test. Coś w stylu:

@Test
public void someTest() {
  Bar bar = getBar();

  // do some things

  verify(mockedObject).someMethod(); // <--- will fail
}

To się nie udaje, ponieważ wyśmiewany obiekt someMethodwywołał się dwukrotnie. Nie chcę, aby moje metody testowe przejmowały się skutkami ubocznymi mojej getBar()metody, więc czy uzasadnione byłoby zresetowanie mojego próbnego obiektu na końcu getBar()?

private Bar getBar() {
  Bar result = new Bar(mockedObject); // this calls mockedObject.someMethod()
  reset(mockedObject); // <-- is this OK?
}

Pytam, ponieważ dokumentacja sugeruje, że resetowanie fałszywych obiektów ogólnie wskazuje na złe testy. Jednak wydaje mi się to w porządku.

Alternatywny

Wydaje się, że alternatywnym wyborem jest wołanie:

verify(mockedObject, times(2)).someMethod();

co moim zdaniem zmusza każdy test do poznania oczekiwań getBar(), bez żadnego zysku.

Duncan Jones
źródło

Odpowiedzi:

60

Uważam, że jest to jeden z przypadków, w których używanie reset()jest w porządku. Test, który piszesz, polega na sprawdzeniu, czy „niektóre rzeczy” wywołują pojedyncze wywołanie someMethod(). Pisanie verify()oświadczenia z dowolną liczbą wywołań może prowadzić do zamieszania.

  • atLeastOnce() pozwala na fałszywe alarmy, co jest złą rzeczą, ponieważ chcesz, aby twoje testy zawsze były poprawne.
  • times(2)zapobiega fałszywemu pozytywowi, ale sprawia wrażenie, jakbyś spodziewał się dwóch wywołań zamiast mówić „wiem, że konstruktor dodaje jedno”. Co więcej, jeśli coś się zmieni w konstruktorze, aby dodać dodatkowe wywołanie, test ma teraz szansę na fałszywie dodatni wynik. Usunięcie wywołania spowoduje, że test zakończy się niepowodzeniem, ponieważ test jest teraz nieprawidłowy zamiast testowanego jest zły.

Używając reset()metody pomocnika, unikniesz obu tych problemów. Należy jednak zachować ostrożność, aby zresetować również wszystkie dokonane kody pośredniczące, więc należy ostrzec. Głównym powodem reset()odradzania jest zapobieganie

bar = mock(Bar.class);
//do stuff
verify(bar).someMethod();
reset(bar);
//do other stuff
verify(bar).someMethod2();

Nie tego próbuje OP. Zakładam, że OP ma test, który weryfikuje wywołanie w konstruktorze. W tym teście reset pozwala na wyizolowanie tego pojedynczego działania i jego efektu. Ten jeden z niewielu przypadków reset()może być pomocny jako. Inne opcje, które nie używają wszystkich, mają wady. Fakt, że OP opublikował ten post, pokazuje, że myśli on o sytuacji, a nie tylko ślepo używa metody resetowania.

unholysampler
źródło
17
Chciałbym, aby Mockito dostarczył funkcję resetInteractions (), aby po prostu zapomnieć o przeszłych interakcjach w celu weryfikacji (..., razy (...)) i utrzymania stubowania. To spowodowałoby, że sytuacje testowe {setup; działać; zweryfikować;} o wiele łatwiej sobie poradzić. Byłoby to {setup; resetInteractions; działać; weryfikacja}
Arkadiy
2
W rzeczywistości od Mockito 2.1 zapewnia on sposób usuwania wywołań bez resetowania kodów pośredniczących:Mockito.clearInvocations(T... mocks)
Colin D Bennett
6

Użytkownicy Smart Mockito prawie nie korzystają z funkcji resetowania, ponieważ wiedzą, że może to być oznaką słabych testów. Zwykle nie trzeba resetować swoich prób, wystarczy utworzyć nowe próby dla każdej metody testowej.

Zamiast reset()zastanowić się nad pisaniem prostych, małych i skoncentrowanych metod testowych nad długimi, zbyt szczegółowymi testami. Pierwszy potencjalny zapach kodu jest reset()w środku metody testowej.

Wyodrębniono z dokumentów mockito .

Radzę, abyś starał się unikać używania reset(). Moim zdaniem, jeśli zadzwonisz dwukrotnie do SomeMethod, powinno to zostać przetestowane (być może jest to dostęp do bazy danych lub inny długi proces, o który chcesz dbać).

Jeśli naprawdę Cię to nie obchodzi, możesz użyć:

verify(mockedObject, atLeastOnce()).someMethod();

Zauważ, że to ostatnie może spowodować fałszywy wynik, jeśli wywołasz metodę getMar z metody getBar, a nie później (jest to niewłaściwe zachowanie, ale test się nie powiedzie).

greuze
źródło
2
Tak, widziałem ten dokładny cytat (odniosłem się do niego z mojego pytania). Obecnie nie widzę przyzwoitego argumentu, dlaczego mój powyższy przykład jest „zły”. Czy możesz podać jeden?
Duncan Jones
Jeśli musisz zresetować próbne obiekty, wygląda na to, że próbujesz przetestować zbyt wiele rzeczy w teście. Możesz podzielić go na dwa testy, testując mniejsze rzeczy. W każdym razie nie wiem, dlaczego weryfikujesz wewnątrz metody getBar, trudno jest śledzić to, co testujesz. Polecam zaprojektować testowe myślenie w oparciu o to, co powinna zrobić klasa (jeśli musisz wywołać metodę SomeMethod dokładnie dwa razy, przynajmniej raz, tylko raz, nigdy itd.) I przeprowadzać każdą weryfikację w tym samym miejscu.
greuze
Zredagowałem swoje pytanie, aby podkreślić, że problem występuje nadal, nawet jeśli nie wywołam verifymojej prywatnej metody (co zgadzam się, prawdopodobnie tam nie należy). Cieszę się z twoich komentarzy na temat tego, czy twoja odpowiedź zmieni się.
Duncan Jones
Istnieje wiele dobrych powodów, aby użyć resetowania, w tym przypadku nie przywiązywałbym zbytniej uwagi do cytatu mockito. Możesz mieć Spring JUnit Class Runner podczas uruchamiania pakietu testowego, powodując niechciane interakcje, szczególnie jeśli wykonujesz testy obejmujące wyśmiewane wywołania bazy danych lub wywołania zawierające prywatne metody, na których nie chcesz używać refleksji.
Sandy Simonton,
Zwykle uważam to za trudne, gdy chcę przetestować wiele rzeczy, ale JUnit po prostu nie oferuje żadnego miłego (!) Sposobu parametryzacji testów. W przeciwieństwie do NUnit robi na przykład z adnotacjami.
Stefan Hendriks
3

Absolutnie nie. Jak to często bywa, trudność w napisaniu czystego testu jest główną czerwoną flagą na temat projektu kodu produkcyjnego. W takim przypadku najlepszym rozwiązaniem jest refaktoryzacja kodu, aby konstruktor Bar nie wywoływał żadnych metod.

Konstruktory powinny konstruować, a nie logikę. Weź wartość zwracaną metody i przekaż ją jako parametr konstruktora.

new Bar(mockedObject);

staje się:

new Bar(mockedObject.someMethod());

Jeśli spowodowałoby to powielenie tej logiki w wielu miejscach, zastanów się nad stworzeniem metody fabrycznej, którą można przetestować niezależnie od obiektu Bar:

public Bar createBar(MockedObject mockedObject) {
    Object dependency = mockedObject.someMethod();
    // ...more logic that used to be in Bar constructor
    return new Bar(dependency);
}

Jeśli refaktoryzacja jest zbyt trudna, dobrym pomysłem jest użycie reset (). Wyjaśnijmy jednak - oznacza to, że kod jest źle zaprojektowany.

tonicsoft
źródło