Mockito Jak kpić tylko z wywołania metody nadklasy

95

Używam Mockito w niektórych testach.

Mam następujące zajęcia:

class BaseService {  
    public void save() {...}  
}

public Childservice extends BaseService {  
    public void save(){  
        //some code  
        super.save();
    }  
}   

Chcę kpić tylko z drugiego wywołania ( super.save) z ChildService. Pierwsze wywołanie musi wywołać prawdziwą metodę. Czy jest na to sposób?

mada
źródło
Czy można to rozwiązać za pomocą PowerMockito?
javaPlease42
@ javaPlease42: Tak, możesz: stackoverflow.com/a/23884011/2049986 .
Jacob van Lingen

Odpowiedzi:

57

Nie, Mockito tego nie obsługuje.

To może nie być odpowiedź, której szukasz, ale to, co widzisz, jest symptomem niestosowania zasady projektowania:

Przedkładaj kompozycję nad dziedziczenie

Jeśli wyodrębnisz strategię zamiast rozszerzać superklasę, problem zniknie.

Jeśli jednak nie możesz zmienić kodu, ale i tak musisz go przetestować, w ten niezręczny sposób, nadal jest nadzieja. Za pomocą niektórych narzędzi AOP (na przykład AspectJ) możesz wplecić kod w metodę superklasy i całkowicie uniknąć jej wykonywania (fuj). To nie działa, jeśli używasz proxy, musisz użyć modyfikacji kodu bajtowego (albo splatanie czasu ładowania, albo tkanie czasu kompilacji). Istnieją również frameworki, które obsługują tego typu sztuczki, takie jak PowerMock i PowerMockito.

Proponuję zdecydować się na refaktoryzację, ale jeśli nie jest to opcja, czeka Cię poważna zabawa w hakowanie.

iwein
źródło
5
Nie widzę naruszenia LSP. Mam mniej więcej taką samą konfigurację jak OP: podstawową klasę DAO z metodą findAll () i podklasę DAO, która zastępuje metodę podstawową, wywołując super.findAll (), a następnie sortując wynik. Podklasę można zastąpić we wszystkich kontekstach akceptujących nadklasę. Czy źle zrozumiałem twoje znaczenie?
1
Usunę uwagę LSP (nie dodaje wartości do odpowiedzi).
Iwein
Tak, dziedziczenie jest do bani, a głupie ramy, w których utknąłem, są zaprojektowane z dziedziczeniem jako jedyną opcją.
Sridhar Sarnobat
Zakładając, że nie możesz przeprojektować nadklasy, możesz wyodrębnić //some codeskod do metody, którą można przetestować oddzielnie.
Phasmal
1
Ok, rozumiem. To inny problem niż ten, który próbowałem rozwiązać, kiedy tego szukałem, ale przynajmniej to nieporozumienie rozwiązało mój własny problem, aby udawać wywołanie z klasy bazowej (którego faktycznie nie nadpisuję).
Guillaume Perrot
89

Jeśli naprawdę nie masz wyboru co do refaktoryzacji, możesz mock / stub wszystko w wywołaniu super metody, np

    class BaseService {

        public void validate(){
            fail(" I must not be called");
        }

        public void save(){
            //Save method of super will still be called.
            validate();
        }
    }

    class ChildService extends BaseService{

        public void load(){}

        public void save(){
            super.save();
            load();
        }
    }

    @Test
    public void testSave() {
        ChildService classToTest = Mockito.spy(new ChildService());

        // Prevent/stub logic in super.save()
        Mockito.doNothing().when((BaseService)classToTest).validate();

        // When
        classToTest.save();

        // Then
        verify(classToTest).load();
    }
jgonian
źródło
2
ten kod w rzeczywistości nie uniemożliwiłby prawidłowego wywołania super.save (), więc jeśli robisz dużo w super.save (), musiałbyś zapobiec tym wszystkim wywołaniom ...
iwein
fantastyczne rozwiązanie działa na mnie cuda, gdy chcę zwrócić fałszywą wartość z metody nadklasy do wykorzystania przez dziecko, fantastycznie dziękuję.
Gurnard
14
działa to dobrze, chyba że walidacja jest ukryta lub metoda save działa bezpośrednio, zamiast wywoływać inną metodę. mockito nie: Mockito.doNothing (). when ((BaseService) spy) .save (); to nie `` nic nie zrobi w podstawowej usłudze save, ale na childService save :(
tibi
Nie mogę zmusić tego do pracy na moim - to również eliminuje metodę potomną. BaseServicejest abstrakcyjny, chociaż nie rozumiem, dlaczego miałoby to mieć znaczenie.
Sridhar Sarnobat
1
@ Sridhar-Sarnobat tak, widzę to samo :( ktoś wie, jak to zrobić, aby tylko super.validate()
zdusić
4

Rozważ refaktoryzację kodu z metody ChildService.save () na inną metodę i przetestuj tę nową metodę zamiast testowania ChildService.save (), w ten sposób unikniesz niepotrzebnego wywoływania metody super.

Przykład:

class BaseService {  
    public void save() {...}  
}

public Childservice extends BaseService {  
    public void save(){  
        newMethod();    
        super.save();
    }
    public void newMethod(){
       //some codes
    }
} 
Mohammed Misbahuddin
źródło
1

utwórz metodę chronioną pakietem (zakłada klasę testową w tym samym pakiecie) w klasie podrzędnej, która wywołuje metodę superklasy, a następnie wywołaj tę metodę w swojej przesłoniętej metodzie podklasy. możesz następnie określić oczekiwania dotyczące tej metody w swoim teście, używając wzorca szpiegowskiego. nie ładna, ale z pewnością lepsza niż konieczność radzenia sobie z wszystkimi oczekiwaniami dotyczącymi super metody w twoim teście

Łukasz
źródło
czy mogę też powiedzieć, że kompozycja zamiast dziedziczenia jest prawie zawsze lepsza, ale czasami po prostu prostsze jest użycie dziedziczenia. dopóki java nie zastosuje lepszego modelu kompozycyjnego, takiego jak scala lub groovy, zawsze tak będzie i ten problem będzie nadal istniał
Luke
1

Nawet jeśli całkowicie zgadzam się z odpowiedzią iwein (

przedkładają kompozycję nad dziedziczenie

), przyznaję, że czasami dziedziczenie wydaje się po prostu naturalne i nie czuję, żebym go łamał lub refaktoryzował tylko ze względu na test jednostkowy.

Tak więc moja sugestia:

/**
 * BaseService is now an asbtract class encapsulating 
 * some common logic callable by child implementations
 */
abstract class BaseService {  
    protected void commonSave() {
        // Put your common work here
    }

    abstract void save();
}

public ChildService extends BaseService {  
    public void save() {
        // Put your child specific work here
        // ...

        this.commonSave();
    }  
}

A potem w teście jednostkowym:

    ChildService childSrv = Mockito.mock(ChildService.class, Mockito.CALLS_REAL_METHODS);

    Mockito.doAnswer(new Answer<Void>() {
        @Override
        public Boolean answer(InvocationOnMock invocation)
                throws Throwable {
            // Put your mocked behavior of BaseService.commonSave() here
            return null;
        }
    }).when(childSrv).commonSave();

    childSrv.save();

    Mockito.verify(childSrv, Mockito.times(1)).commonSave();

    // Put any other assertions to check child specific work is done
dams50
źródło
0

Powodem jest to, że twoja klasa bazowa nie jest publicznie dostępna, a Mockito nie może jej przechwycić ze względu na widoczność, jeśli zmienisz klasę bazową na publiczną lub @Override w klasie podrzędnej (jako publiczną), wtedy Mockito może ją poprawnie kpić.

public class BaseService{
  public boolean foo(){
    return true;
  }
}

public ChildService extends BaseService{
}

@Test
@Mock ChildService childService;
public void testSave() {
  Mockito.when(childService.foo()).thenReturn(false);

  // When
  assertFalse(childService.foo());
}
zoufeiyy
źródło
8
Nie o to chodzi. ChildService powinno zastąpić foo (), a problem polega na tym, jak wyszydzić BaseService.foo (), ale nie ChildService.foo ()
Adriaan Koster
0

Może najłatwiejszą opcją, jeśli dziedziczenie ma sens, jest utworzenie nowej metody (pakiet prywatny ??), aby wywołać super (nazwijmy to superFindall), szpiegować prawdziwą instancję, a następnie kpić z metody superFindAll () w sposób, w jaki chciałeś kpić pierwsza klasa rodzicielska. Nie jest to idealne rozwiązanie pod względem zasięgu i widoczności, ale powinno wystarczyć i jest łatwe w aplikacji.

 public Childservice extends BaseService {
    public void save(){
        //some code
        superSave();
    }

    void superSave(){
        super.save();
    }
}
Rubasace
źródło