2012-01-24 18 views
91

Jestem początkującym programistą, w szczególności dla rozwoju i testów jednostkowych. Domyślam się, że moje wymagania są dość proste, ale chętnie znam inne myśli na ten temat.Prześmiewanie zmiennych członków klasy przy użyciu Mockito

Załóżmy, że mam dwie klasy - tak jak

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 piszę badanej jednostki przetestować First.doSecond() metody. Jednak przypuśćmy, że chcę wykpić klasę Second.doSecond(). Używam Mockito, aby to zrobić.

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 szyderstwo nie przynosi skutku, a twierdzenie nie powiedzie się. Czy nie można kpić z zmiennych członkowskich klasy, którą chcę przetestować. ?

Odpowiedz

58

Musisz podać sposób dostępu do zmiennych członkowskich, aby można było wprowadzić w nim symulację (najczęstsze sposoby byłaby to metoda ustawiająca lub konstruktor, który przyjmuje parametr).

Jeśli twój kod nie zapewnia tego sposobu, jest nieprawidłowo obliczony dla TDD (Test Driven Development).

+1

Dzięki. Widzę to. Zastanawiam się tylko, jak mogę wtedy przeprowadzić testy integracyjne za pomocą makiety, gdzie może być wiele wewnętrznych metod, klas, które mogą wymagać kpiny, ale niekoniecznie dostępne, aby być ustawione przez setXXX() przed ręką. –

+2

Użyj szkieletu wtrysku zależności, z konfiguracją testu. Sporządź diagram sekwencji testu integracji, który próbujesz wykonać. Zanurz diagram sekwencji w obiektach, które możesz kontrolować. Oznacza to, że jeśli pracujesz z klasą szkieletową, która ma przeciwny wzorzec obiektu zależnego, który pokazałeś powyżej, powinieneś traktować obiekt i jego źle skomponowany element jako pojedynczą jednostkę pod względem diagramu sekwencji. Bądź przygotowany, aby dostosować faktoring dowolnego kodu, który kontrolujesz, aby był bardziej sprawdzalny. – kittylyst

+4

Drogi @kittylyst, tak prawdopodobnie jest on błędny z punktu widzenia TDD lub z dowolnego racjonalnego punktu widzenia. Czasami jednak programista działa w miejscach, w których nic nie ma sensu, a jedynym celem, jaki ma się do spełnienia, jest ukończenie przypisanych historii i odejście. Tak, to jest złe, nie ma sensu, niewykwalifikowani ludzie podejmują kluczowe decyzje i wszystkie te rzeczy. Pod koniec dnia anty-wzorce wygrywają bardzo często. – amanas

30

Jeśli przyjrzysz się bliżej swojemu kodowi, zobaczysz, że właściwość second w teście nadal jest instancją Second, a nie sztuczną (w swoim kodzie nie przekazujesz fałszywej poprawki do first).

Najprostszym sposobem byłoby utworzenie setera dla klasy second w klasie First i przekazanie go w sposób jawny.

jak poniżej:

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 będzie przechodzić wystąpienie Second jako parametr konstruktora First „S.

Jeśli nie można zmodyfikować kod, myślę, że jedyną opcją byłoby użyć refleksji:

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 pewnie można, gdyż rzadko zdarza się robić testy na kod nie kontrolnych (choć można sobie wyobrazić scenariusz, w którym trzeba przetestować zewnętrzną bibliotekę, bo jej autor nie zrobił :))

+0

Mam to. Prawdopodobnie pójdę z twoją pierwszą sugestią. –

+0

Po prostu ciekawy, czy istnieje jakiś sposób lub API, o których wiesz, że może kpić z obiektu/metody na poziomie aplikacji lub pakietu. ? Sądzę, że to, co mówię, to w powyższym przykładzie, kiedy kpię z "drugiego" obiektu, czy istnieje sposób, który może zastąpić każdą instancję Drugiego, która jest używana przez cykl życia testów. ? –

+0

@AnandHemmige faktycznie drugi (konstruktor) jest czystszy, ponieważ unika tworzenia niepotrzebnych instancji 'Second '. Twoje zajęcia są w ten sposób rozdzielone. – soulcheck

42

Nie jest to możliwe, jeśli nie można zmienić kodu. Ale lubię iniekcji zależność i Mockito wspiera go:

public class First {  
    @Resource 
    Second second; 

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

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

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()); 
    } 
} 

Jest to bardzo miłe i łatwe.

+0

Myślę, że jest to lepsza odpowiedź niż inne, ponieważ InjectMocks. – sudocoder

+0

To zabawne, jak się dostaje, jako testujący nowicjusz, jak ja, ufać pewnym bibliotekom i frameworkom. Zakładałem, że to tylko zły pomysł wskazujący na potrzebę przeprojektowania ... dopóki mi tego nie pokazałeś ** jest ** rzeczywiście możliwe (bardzo wyraźnie i czysto) w Mockito. –

+3

Co to jest ** @ Zasób **? –

-1

Tak, można to zrobić, jak następujących pokazach testowych (pisane z JMockit szyderczy API, które rozwijają):

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

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

Z Mockito jednak taki test może nie być napisane. Wynika to z tego, że w Mockito zaimplementowano szyderstwo, w którym tworzona jest podklasa klasy wyśmiewanej; tylko instancje tej "podrabianej" podklasy mogą mieć wyśmiewane zachowanie, więc musisz mieć przetestowany kod, aby użyć ich zamiast jakiejkolwiek innej instancji.

+3

pytanie nie brzmiało, czy JMockit jest lepszy od Mockito, ale raczej jak to zrobić w Mockito. Trzymaj się budowania lepszego produktu zamiast szukać okazji do wyrzucenia konkurencji! – TheZuck

+8

Oryginalny plakat mówi tylko, że używa Mockito; zakłada się jedynie, że Mockito jest stałym i twardym wymogiem, więc wskazówka, że ​​JMockit poradzi sobie z tą sytuacją, nie jest niewłaściwa. – Bombe

6

Jeśli nie można zmienić zmienną składową, a potem na odwrót to jest użycie powerMockit i nazywają

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

Teraz problemem jest to, że każde wezwanie do nowej sekundę spowoduje powrót tą samą instancję szydzili. Ale w twoim prostym przypadku to zadziała.

1

Wiele osób już poradziło ci, abyś ponownie przemyślał swój kod, aby był bardziej testowalny - dobra rada i zwykle prostsza niż to, co mam zamiar zasugerować.

Jeśli nie można zmienić kod, aby uczynić go bardziej sprawdzalne, PowerMock: https://code.google.com/p/powermock/

PowerMock rozciąga Mockito (więc nie trzeba uczyć się nowych ram makiety), zapewniając dodatkową funkcjonalność. Obejmuje to możliwość zwracania przez konstruktora makiety. Potężny, ale trochę skomplikowany - więc używaj go rozważnie.

Używasz innej prowadnicy próbnej. I musisz przygotować klasę, która ma wywołać konstruktora. (Zauważ, że jest to wspólny haczyka - przygotować klasę, która wywołuje konstruktora, a nie zbudowane klasa)

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

Następnie w testowym set-up, można użyć metody whenNew mieć konstruktor zwróci makiety

whenNew(Second.class).withAnyArguments().thenReturn(mock(Second.class)); 
5

Miałem ten sam problem, gdy wartość prywatna nie została ustawiona, ponieważ Mockito nie nazywa super konstruktorów. Oto jak poszerzam kpiny z refleksji.

Najpierw stworzyłem klasę TestUtils, która zawiera wiele przydatnych narzędzi, w tym te metody refleksji. Dostęp refleksyjny jest za każdym razem trochę zaimplementowany. Stworzyłem te metody, aby przetestować kod na projektach, które z tego czy innego powodu nie zawierały kpiącego pakietu i nie zaproszono mnie do jego włączenia.

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ę z prywatną zmienną taką jak ta. Przydaje się to do szyderstwa w drzewach klas, które również nie mają 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. Może wystąpić problem kompilacji lub dwóch. Myślę, że masz ogólny pomysł. Możesz pobrać kod i użyć go, jeśli uznasz to za przydatne.

+0

Czy mógłbyś wyjaśnić swój kod faktycznym użyciem? jak Klasa publiczna tobeMocker() { private ClassObject classObject; } Gdzie classObject jest równy obiektowi, który ma zostać zastąpiony. –

+0

W twoim przykładzie, jeśli ToBeMocker instance = new ToBeMocker(); i ClassObject someNewInstance = new ClassObject() { @Override // coś jak zewnętrzna zależność }; następnie TestUtils.refelctSetValue (instancja, "classObject", someNewInstance); Pamiętaj, że musisz wymyślić, co chcesz zastąpić przed kpiną. Powiedzmy, że masz bazę danych, a to zastąpienie zwróci wartość, więc nie musisz wybierać. Ostatnio miałem autobus serwisowy, którego nie chciałem przetwarzać, ale chciałem upewnić się, że go otrzymał. W ten sposób ustawię instancję prywatnego autobusu w ten sposób - Pomocna? – dave

+0

Musisz sobie wyobrazić, że w tym komentarzu było formatowanie. Został usunięty. Ponadto nie będzie działać z Javą 9, ponieważ zablokuje dostęp prywatny. Będziemy musieli pracować z kilkoma innymi konstrukcjami, gdy tylko otrzymamy oficjalną wersję i będziemy mogli pracować z jej rzeczywistymi granicami. – dave

Powiązane problemy