Mockowanie zmiennych składowych klasy przy użyciu Mockito

142

Jestem nowicjuszem w programowaniu, aw szczególności w testach jednostkowych. Myślę, że moje wymagania są dość proste, ale chciałbym poznać opinie innych na ten temat.

Załóżmy, że mam dwie takie klasy -

public class First {

    Second second ;

    public First(){
        second = new Second();
    }

    public String doSecond(){
        return second.doSecond();
    }
}

class Second {

    public String doSecond(){
        return "Do Something";
    }
}

Powiedzmy, że piszę test jednostkowy, aby przetestować First.doSecond()metodę. Jednak przypuśćmy, że chcę Mock Second.doSecond()klasę w ten sposób. Do tego używam Mockito.

public void testFirst(){
    Second sec = mock(Second.class);
    when(sec.doSecond()).thenReturn("Stubbed Second");

    First first = new First();
    assertEquals("Stubbed Second", first.doSecond());
}

Widzę, że kpiny nie przynoszą efektu, a stwierdzenie zawodzi. Czy nie ma sposobu na mockowanie zmiennych składowych klasy, którą chcę przetestować. ?

Anand Hemmige
źródło

Odpowiedzi:

88

Musisz zapewnić sposób uzyskiwania dostępu do zmiennych składowych, aby można było je przekazać w makiecie (najczęstszym sposobem jest metoda ustawiająca lub konstruktor, który przyjmuje parametr).

Jeśli Twój kod nie umożliwia tego, jest niepoprawnie uwzględniany w TDD (Test Driven Development).

kittylyst
źródło
4
Dzięki. Widzę to. Zastanawiam się tylko, jak mogę następnie przeprowadzić testy integracji przy użyciu makiety, gdzie może istnieć wiele metod wewnętrznych, klas, które mogą wymagać mockowania, ale niekoniecznie są dostępne do ustawienia za pomocą metody setXXX ().
Anand Hemmige
2
Użyj struktury iniekcji zależności z konfiguracją testową. Sporządź diagram sekwencji testu integracji, który próbujesz wykonać. Uwzględnij diagram sekwencji w obiektach, które możesz faktycznie kontrolować. Oznacza to, że jeśli pracujesz z klasą frameworkową, która ma zależny obiekt anty-wzorzec, który pokazałeś powyżej, powinieneś traktować obiekt i jego źle podzielony na czynniki element składowy jako pojedynczą jednostkę w kategoriach diagramu sekwencji. Przygotuj się na dostosowanie faktoringu dowolnego kontrolowanego kodu, aby był bardziej testowalny.
kittylyst
9
Drogi @kittylyst, tak, prawdopodobnie jest to błędne z punktu widzenia TDD lub z jakiegokolwiek racjonalnego punktu widzenia. Ale czasami programista pracuje w miejscach, w których nic nie ma sensu, a jedynym celem, jaki ma, jest ukończenie przypisanych historii i odejście. Tak, to źle, to nie ma sensu, niewykwalifikowani ludzie podejmują kluczowe decyzje i tak dalej. Ostatecznie więc anty-wzorce wygrywają dużo.
amanas
1
Ciekawi mnie to, jeśli członek klasy nie ma powodu, aby ustawiać go z zewnątrz, dlaczego mielibyśmy tworzyć ustawiacz tylko w celu przetestowania go? Wyobraź sobie, że klasa „druga” jest w rzeczywistości menedżerem lub narzędziem FileSystem, zainicjowanym podczas konstruowania obiektu do testowania. Mam wszelkie powody, aby chcieć kpić z tego menedżera FileSystem, aby przetestować pierwszą klasę i nie ma powodu, aby był dostępny. Mogę to zrobić w Pythonie, więc dlaczego nie z Mockito?
Zangdar
67

Nie jest to możliwe, jeśli nie możesz zmienić kodu. Ale lubię wstrzykiwanie zależności i Mockito to obsługuje:

public class First {    
    @Resource
    Second second;

    public First() {
        second = new Second();
    }

    public String doSecond() {
        return second.doSecond();
    }
}

Twój test:

@RunWith(MockitoJUnitRunner.class)
public class YourTest {
   @Mock
   Second second;

   @InjectMocks
   First first = new First();

   public void testFirst(){
      when(second.doSecond()).thenReturn("Stubbed Second");
      assertEquals("Stubbed Second", first.doSecond());
   }
}

To bardzo przyjemne i łatwe.

Janning
źródło
2
Myślę, że to lepsza odpowiedź niż inne, ponieważ InjectMocks.
sudocoder
To zabawne, jak jako początkujący tester, taki jak ja, można zaufać pewnym bibliotekom i frameworkom. Zakładałem, że to tylko zły pomysł wskazujący na potrzebę przeprojektowania ... dopóki nie pokazałeś mi, że jest to rzeczywiście możliwe (bardzo wyraźnie i czysto) w Mockito.
mike gryzoń
9
Co to jest @Resource ?
Igor Ganapolsky
3
@IgorGanapolsky @ Zasób to adnotacja utworzona / używana przez platformę Java Spring. Jest to sposób na wskazanie Springowi, że jest to fasola / obiekt zarządzany przez Spring. stackoverflow.com/questions/4093504/resource-vs-autowired baeldung.com/spring-annotations-resource-inject-autowire To nie jest rzecz mockito, ale ponieważ jest używana w klasie nietestującej, musi być wyśmiana w test.
Grez.Kev
Nie rozumiem tej odpowiedzi. Mówisz, że to niemożliwe, a potem pokazujesz, że to możliwe? Co właściwie nie jest tutaj możliwe?
StackOverflowOfficial
35

Jeśli przyjrzysz się uważnie swojemu kodowi, zobaczysz, że secondwłaściwość w twoim teście jest nadal instancją Second, a nie próbą (nie przekazujesz jej firstw kodzie).

Najprostszym sposobem byłoby utworzenie metody ustawiającej secondw Firstklasie i jawne przekazanie jej jako makiety.

Lubię to:

public class First {

Second second ;

public First(){
    second = new Second();
}

public String doSecond(){
    return second.doSecond();
}

    public void setSecond(Second second) {
    this.second = second;
    }


}

class Second {

public String doSecond(){
    return "Do Something";
}
}

....

public void testFirst(){
Second sec = mock(Second.class);
when(sec.doSecond()).thenReturn("Stubbed Second");


First first = new First();
first.setSecond(sec)
assertEquals("Stubbed Second", first.doSecond());
}

Innym byłoby przekazanie Secondinstancji jako Firstparametru konstruktora.

Jeśli nie możesz zmodyfikować kodu, myślę, że jedyną opcją byłoby użycie odbicia:

public void testFirst(){
    Second sec = mock(Second.class);
    when(sec.doSecond()).thenReturn("Stubbed Second");


    First first = new First();
    Field privateField = PrivateObject.class.
        getDeclaredField("second");

    privateField.setAccessible(true);

    privateField.set(first, sec);

    assertEquals("Stubbed Second", first.doSecond());
}

Ale prawdopodobnie tak, ponieważ rzadko wykonuje się testy na kodzie, nad którym nie kontrolujesz (chociaż można sobie wyobrazić scenariusz, w którym musisz przetestować zewnętrzną bibliotekę, ponieważ jej autor nie :))

soulcheck
źródło
Rozumiem. Prawdopodobnie pójdę z twoją pierwszą sugestią.
Anand Hemmige
Ciekawe, czy istnieje sposób lub interfejs API, o którym wiesz, że może kpić z obiektu / metody na poziomie aplikacji lub na poziomie pakietu. ? Wydaje mi się, że w powyższym przykładzie mówię, kiedy kpię z obiektu „Second”, czy istnieje sposób, aby nadpisać każdą instancję Second, która jest używana przez cały cykl testowania. ?
Anand Hemmige
@AnandHemmige właściwie druga (konstruktor) jest czystsza, ponieważ unika tworzenia niepotrzebnych instancji `Second´. W ten sposób Twoje klasy są ładnie rozdzielone.
soulcheck
10
Mockito zapewnia kilka fajnych adnotacji, które pozwolą ci wstrzyknąć swoje makiety do zmiennych prywatnych. Dodaj adnotację do Second za pomocą @Mocki dodaj adnotację First za pomocą @InjectMocksi utwórz wystąpienie First w inicjatorze. Mockito automatycznie zrobi najlepiej, aby znaleźć miejsce do wstrzyknięcia drugiej makiety do pierwszej instancji, w tym ustawienie pól prywatnych, które pasują do typu.
jhericks
@Mockbyło w okolicach 1.5 (może wcześniej, nie jestem pewien). 1.8.3 wprowadzono, @InjectMocksa także @Spyi @Captor.
jhericks
7

Jeśli nie możesz zmienić zmiennej składowej, to na odwrót jest użycie powerMockit i call

Second second = mock(Second.class)
when(second.doSecond()).thenReturn("Stubbed Second");
whenNew(Second.class).withAnyArguments.thenReturn(second);

Problem w tym, że KAŻDE wywołanie nowej Second zwróci tę samą udaną instancję. Ale w twoim prostym przypadku to zadziała.

user1509463
źródło
7

Miałem ten sam problem, w którym prywatna wartość nie została ustawiona, ponieważ Mockito nie wywołuje super konstruktorów. Oto jak potęguję kpiny refleksją.

Najpierw utworzyłem klasę TestUtils, która zawiera wiele pomocnych narzędzi, w tym metody odbicia. Dostęp do refleksji jest za każdym razem nieco trudny do wdrożenia. Stworzyłem te metody, aby przetestować kod w projektach, które z jakiegoś powodu nie miały pakietu symulującego i nie zostałem zaproszony do włączenia go.

public class TestUtils {
    // get a static class value
    public static Object reflectValue(Class<?> classToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField  = reflectField(classToReflect, fieldNameValueToFetch);
            reflectField.setAccessible(true);
            Object reflectValue = reflectField.get(classToReflect);
            return reflectValue;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch);
        }
        return null;
    }
    // get an instance value
    public static Object reflectValue(Object objToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField  = reflectField(objToReflect.getClass(), fieldNameValueToFetch);
            Object reflectValue = reflectField.get(objToReflect);
            return reflectValue;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch);
        }
        return null;
    }
    // find a field in the class tree
    public static Field reflectField(Class<?> classToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField = null;
            Class<?> classForReflect = classToReflect;
            do {
                try {
                    reflectField = classForReflect.getDeclaredField(fieldNameValueToFetch);
                } catch (NoSuchFieldException e) {
                    classForReflect = classForReflect.getSuperclass();
                }
            } while (reflectField==null || classForReflect==null);
            reflectField.setAccessible(true);
            return reflectField;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch +" from "+ classToReflect);
        }
        return null;
    }
    // set a value with no setter
    public static void refectSetValue(Object objToReflect, String fieldNameToSet, Object valueToSet) {
        try {
            Field reflectField  = reflectField(objToReflect.getClass(), fieldNameToSet);
            reflectField.set(objToReflect, valueToSet);
        } catch (Exception e) {
            fail("Failed to reflectively set "+ fieldNameToSet +"="+ valueToSet);
        }
    }

}

Następnie mogę przetestować klasę za pomocą takiej zmiennej prywatnej. Jest to przydatne do wyszydzania głęboko w drzewach klas, nad którymi również nie masz kontroli.

@Test
public void testWithRectiveMock() throws Exception {
    // mock the base class using Mockito
    ClassToMock mock = Mockito.mock(ClassToMock.class);
    TestUtils.refectSetValue(mock, "privateVariable", "newValue");
    // and this does not prevent normal mocking
    Mockito.when(mock.somthingElse()).thenReturn("anotherThing");
    // ... then do your asserts
}

Zmodyfikowałem mój kod z mojego aktualnego projektu tutaj, na stronie. Mogą wystąpić problemy z kompilacją lub dwa. Myślę, że masz ogólny pomysł. Możesz pobrać kod i użyć go, jeśli uznasz to za przydatne.

dave
źródło
Czy mógłbyś wyjaśnić swój kod za pomocą rzeczywistego zastosowania? jak Public class tobeMocker () {private ClassObject classObject; } Gdzie classObject jest równe obiektowi do zastąpienia.
Jasper Lankhorst
W twoim przykładzie, jeśli ToBeMocker instance = new ToBeMocker (); i ClassObject someNewInstance = new ClassObject () {@Override // coś w rodzaju zewnętrznej zależności}; następnie TestUtils.refelctSetValue (instancja, "classObject", someNewInstance); Zauważ, że musisz dowiedzieć się, co chcesz przesłonić w przypadku kpiny. Powiedzmy, że masz bazę danych i to zastąpienie zwróci wartość, więc nie musisz wybierać. Ostatnio miałem magistralę usługową, której nie chciałem faktycznie przetwarzać wiadomości, ale chciałem mieć pewność, że ją otrzyma. Dlatego ustawiam instancję prywatnej magistrali w ten sposób - Przydatne?
dave
Będziesz musiał sobie wyobrazić, że w tym komentarzu było formatowanie. Został usunięty. Nie będzie to również działać z Javą 9, ponieważ blokuje prywatny dostęp. Będziemy musieli pracować z kilkoma innymi konstrukcjami, gdy już będziemy mieć oficjalne wydanie i będziemy mogli pracować z jego rzeczywistymi ograniczeniami.
Dave
1

Wiele innych osób już radziło ci, aby przemyśleć swój kod, aby był bardziej testowalny - dobra rada i zwykle prostsza niż to, co zamierzam zasugerować.

Jeśli nie możesz zmienić kodu, aby był bardziej testowalny, PowerMock: https://code.google.com/p/powermock/

PowerMock rozszerza Mockito (więc nie musisz uczyć się nowego frameworka), zapewniając dodatkowe funkcje. Obejmuje to możliwość zwrócenia przez konstruktora makiety. Potężny, ale trochę skomplikowany - więc używaj go rozsądnie.

Używasz innego Mock runner. I musisz przygotować klasę, która będzie wywoływała konstruktor. (Zwróć uwagę, że jest to powszechna metoda - przygotuj klasę, która wywołuje konstruktora, a nie skonstruowaną klasę)

@RunWith(PowerMockRunner.class)
@PrepareForTest({First.class})

Następnie w konfiguracji testowej możesz użyć metody whenNew, aby konstruktor zwrócił makietę

whenNew(Second.class).withAnyArguments().thenReturn(mock(Second.class));
jwepurchase
źródło
0

Tak, można to zrobić, jak pokazuje poniższy test (napisany za pomocą JMockit mocking API, które tworzę):

@Test
public void testFirst(@Mocked final Second sec) {
    new NonStrictExpectations() {{ sec.doSecond(); result = "Stubbed Second"; }};

    First first = new First();
    assertEquals("Stubbed Second", first.doSecond());
}

Jednak w przypadku Mockito takiego testu nie da się napisać. Wynika to ze sposobu, w jaki mockowanie jest zaimplementowane w Mockito, gdzie tworzona jest podklasa klasy, która ma być mockowana; tylko instancje tej „pozorowanej” podklasy mogą mieć fałszywe zachowanie, więc testowany kod powinien używać ich zamiast innych instancji.

Rogério
źródło
3
nie chodziło o to, czy JMockit jest lepszy od Mockito, ale raczej jak to zrobić w Mockito. Staraj się budować lepszy produkt, zamiast szukać okazji do rozgromienia konkurencji!
TheZuck,
8
Oryginalny plakat mówi tylko, że używa Mockito; sugeruje się tylko, że Mockito jest stałym i trudnym wymaganiem, więc wskazówka, że ​​JMockit może sobie z tym poradzić, nie jest tak niewłaściwa.
Bombe
0

Jeśli chcesz mieć alternatywę dla ReflectionTestUtils ze Springa w mockito, użyj

Whitebox.setInternalState(first, "second", sec);
Szymon Zwoliński
źródło
Witamy w Stack Overflow! Istnieją inne odpowiedzi, które zawierają pytanie PO, i zostały one opublikowane wiele lat temu. Pisząc odpowiedź, upewnij się, że dodałeś nowe rozwiązanie lub znacznie lepsze wyjaśnienie, zwłaszcza gdy odpowiadasz na starsze pytania lub komentujesz inne odpowiedzi.
help-info.de
0

Możesz mockować dowolną zmienną składową Mockito Mock za pomocą ReflectionTestUtils

ReflectionTestUtils.setField(yourMock, "memberFieldName", value);
Gayan Weerakutti
źródło