Używanie Mockito do testowania klas abstrakcyjnych

213

Chciałbym przetestować klasę abstrakcyjną. Jasne, mogę ręcznie napisać próbną dziedziczenie po klasie.

Czy mogę to zrobić przy użyciu frameworku (używam Mockito) zamiast ręcznego tworzenia mojej makiety? W jaki sposób?

ripper234
źródło
2
Począwszy od wersji Mockito 1.10.12 , Mockito bezpośrednio obsługuje szpiegostwo / drwiny z abstrakcyjnych klas:SomeAbstract spy = spy(SomeAbstract.class);
pesche
6
Począwszy od Mockito 2.7.14, można również makiety abstrakcyjne classess które wymagają argumenty konstruktora poprzezmock(MyAbstractClass.class, withSettings().useConstructor(arg1, arg2).defaultAnswer(CALLS_REAL_METHODS))
Gediminas Rimsa

Odpowiedzi:

315

Poniższa sugestia pozwala przetestować klasy abstrakcyjne bez tworzenia „prawdziwej” podklasy - Mock jest podklasą.

użyj Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS), a następnie wyśmiewaj się z wywoływanych metod abstrakcyjnych.

Przykład:

public abstract class My {
  public Result methodUnderTest() { ... }
  protected abstract void methodIDontCareAbout();
}

public class MyTest {
    @Test
    public void shouldFailOnNullIdentifiers() {
        My my = Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS);
        Assert.assertSomething(my.methodUnderTest());
    }
}

Uwaga: Zaletą tego rozwiązania jest to, że nie trzeba implementować metod abstrakcyjnych, o ile nigdy się ich nie wywołuje.

Moim szczerym zdaniem jest to bardziej rozsądne niż używanie szpiega, ponieważ szpieg wymaga instancji, co oznacza, że ​​musisz utworzyć instancję możliwą do natychmiastowego utworzenia podklasy swojej klasy abstrakcyjnej.

Morten Lauritsen Khodabocus
źródło
14
Jak zauważono poniżej, nie działa to, gdy klasa abstrakcyjna wywołuje metody abstrakcyjne w celu przetestowania, co często ma miejsce.
Richard Nichols,
11
To faktycznie działa, gdy klasa abstrakcyjna wywołuje metody abstrakcyjne. Wystarczy użyć składni doReturn lub doNothing zamiast Mockito.when do krojenia metod abstrakcyjnych, a jeśli kiszą jakieś konkretne wywołania, upewnij się, że kasowanie abstrakcyjnych wywołań jest najważniejsze.
Gonen I
2
Jak wstrzykiwać zależności w tego rodzaju obiektach (kpiąca klasa abstrakcyjna wywołująca prawdziwe metody)?
Samuel
2
Zachowuje się to w nieoczekiwany sposób, jeśli dana klasa ma inicjalizatory instancji. Mockito pomija inicjalizatory prób, co oznacza, że ​​zmienne instancji, które są inicjowane inline, będą nieoczekiwanie zerowe, co może powodować NPE.
digitalbath
1
Co jeśli konstruktor klasy abstrakcyjnej przyjmuje jeden lub więcej parametrów?
SD
68

Jeśli potrzebujesz tylko przetestować niektóre konkretne metody bez dotykania któregokolwiek z abstrakcji, możesz użyć CALLS_REAL_METHODS(patrz odpowiedź Mortena ), ale jeśli konkretna testowana metoda wywołuje niektóre abstrakty lub niezaimplementowane metody interfejsu, to nie zadziała - Mockito narzeka: „Nie można wywołać prawdziwej metody interfejsu java”.

(Tak, to kiepski projekt, ale niektóre frameworki, np. Tapestry 4, w pewien sposób narzucają go tobie.)

Obejściem tego problemu jest odwrócenie tego podejścia - skorzystaj ze zwykłego próbnego zachowania (tj. Wszystko jest wyśmiewane / zakopane) i użyj, doCallRealMethod()aby jawnie wywołać konkretną testowaną metodę. Na przykład

public abstract class MyClass {
    @SomeDependencyInjectionOrSomething
    public abstract MyDependency getDependency();

    public void myMethod() {
        MyDependency dep = getDependency();
        dep.doSomething();
    }
}

public class MyClassTest {
    @Test
    public void myMethodDoesSomethingWithDependency() {
        MyDependency theDependency = mock(MyDependency.class);

        MyClass myInstance = mock(MyClass.class);

        // can't do this with CALLS_REAL_METHODS
        when(myInstance.getDependency()).thenReturn(theDependency);

        doCallRealMethod().when(myInstance).myMethod();
        myInstance.myMethod();

        verify(theDependency, times(1)).doSomething();
    }
}

Zaktualizowano, aby dodać:

W przypadku metod innych niż void musisz użyć thenCallRealMethod()zamiast tego, np .:

when(myInstance.myNonVoidMethod(someArgument)).thenCallRealMethod();

W przeciwnym razie Mockito będzie skarżyć się na „Wykryto niedokończone kodowanie”.

David Moles
źródło
9
W niektórych przypadkach będzie to działać, jednak Mockito nie wywołuje konstruktora podstawowej klasy abstrakcyjnej za pomocą tej metody. Może to spowodować awarię „rzeczywistej metody” z powodu utworzenia nieoczekiwanego scenariusza. Dlatego też ta metoda nie będzie działać we wszystkich przypadkach.
Richard Nichols,
3
Tak, nie możesz w ogóle liczyć na stan obiektu, tylko kod w wywoływanej metodzie.
David Moles,
Och, więc metody obiektowe zostają oddzielone od stanu, świetnie.
haelix,
17

Możesz to osiągnąć za pomocą szpiega (użyj najnowszej wersji Mockito 1.8+).

public abstract class MyAbstract {
  public String concrete() {
    return abstractMethod();
  }
  public abstract String abstractMethod();
}

public class MyAbstractImpl extends MyAbstract {
  public String abstractMethod() {
    return null;
  }
}

// your test code below

MyAbstractImpl abstractImpl = spy(new MyAbstractImpl());
doReturn("Blah").when(abstractImpl).abstractMethod();
assertTrue("Blah".equals(abstractImpl.concrete()));
Richard Nichols
źródło
14

Frameworki są zaprojektowane tak, aby ułatwić wyśmiewanie się z zależności testowanej klasy. Gdy używasz frameworku do wyśmiewania klasy, większość frameworków dynamicznie tworzy podklasę i zastępuje implementację metody kodem wykrywającym, kiedy metoda jest wywoływana i zwraca fałszywą wartość.

Podczas testowania klasy abstrakcyjnej chcesz wykonać nieabstrakcyjne metody obiektu podlegającego testowi (SUT), więc ramy próbne nie są tym, czego potrzebujesz.

Częściowe zamieszanie polega na tym, że odpowiedź na pytanie, które podałeś, mówiła o stworzeniu kpiny z twojej abstrakcyjnej klasy. Nie nazwałbym takiej klasy kpiną. Mock to klasa, która jest używana jako zamiennik zależności, jest programowana z oczekiwaniami i może być zapytana, czy te oczekiwania są spełnione.

Zamiast tego sugeruję zdefiniowanie nie-abstrakcyjnej podklasy klasy abstrakcyjnej w teście. Jeśli skutkuje to zbyt dużym kodem, może to oznaczać, że twoja klasa jest trudna do rozszerzenia.

Alternatywnym rozwiązaniem byłoby sprawienie, aby sam przypadek testowy był abstrakcyjny, przy użyciu abstrakcyjnej metody tworzenia SUT (innymi słowy, przypadek testowy użyłby wzorca projektowego Metoda szablonowa).

NamshubWriter
źródło
8

Spróbuj użyć niestandardowej odpowiedzi.

Na przykład:

import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

public class CustomAnswer implements Answer<Object> {

    public Object answer(InvocationOnMock invocation) throws Throwable {

        Answer<Object> answer = null;

        if (isAbstract(invocation.getMethod().getModifiers())) {

            answer = Mockito.RETURNS_DEFAULTS;

        } else {

            answer = Mockito.CALLS_REAL_METHODS;
        }

        return answer.answer(invocation);
    }
}

Zwróci próbkę metod abstrakcyjnych i wywoła prawdziwą metodę konkretnych metod.


źródło
5

To, co naprawdę sprawia, że ​​czuję się źle z wyśmiewaniem klas abstrakcyjnych, to fakt, że ani domyślny konstruktor YourAbstractClass () nie jest wywoływany (brakuje super () w próbnym), ani nie wydaje się, aby Mockito mógł domyślnie inicjować właściwości próbne (np. Właściwości listy z pustą ArrayList lub LinkedList).

Moja klasa abstrakcyjna (w zasadzie generowany jest kod źródłowy klasy) NIE zapewnia wstrzykiwania ustawiacza zależności dla elementów listy, ani konstruktora, w którym inicjuje elementy listy (które próbowałem dodać ręcznie).

Tylko atrybuty klasy używają domyślnej inicjalizacji: private List dep1 = new ArrayList; private List dep2 = new ArrayList

Nie ma więc sposobu na wyśmiewanie się klasy abstrakcyjnej bez użycia rzeczywistej implementacji obiektu (np. Definicja klasy wewnętrznej w klasie testów jednostkowych, przesłanianie metod abstrakcyjnych) i szpiegowanie rzeczywistego obiektu (która dokonuje właściwej inicjalizacji pola).

Szkoda, że ​​tylko PowerMock pomógłby tutaj dalej.

Thomas Heiss
źródło
2

Zakładając, że twoje klasy testowe znajdują się w tym samym pakiecie (w innym źródłowym katalogu głównym), co klasy testowane, możesz po prostu utworzyć próbkę:

YourClass yourObject = mock(YourClass.class);

i wywołaj metody, które chcesz przetestować, tak jak każdą inną metodę.

Musisz podać oczekiwania dla każdej metody, która jest wywoływana, z oczekiwaniem na dowolne konkretne metody wywołujące super metodę - nie jestem pewien, jak byś to zrobił za pomocą Mockito, ale wierzę, że jest to możliwe z EasyMock.

Wszystko to polega na stworzeniu konkretnego wystąpienia YouClassi oszczędzeniu wysiłku polegającego na zapewnieniu pustych implementacji każdej metody abstrakcyjnej.

Nawiasem mówiąc, często uważam za użyteczne zaimplementowanie klasy abstrakcyjnej w moim teście, gdzie służy ona jako przykładowa implementacja, którą testuję za pomocą jej publicznego interfejsu, chociaż zależy to od funkcjonalności zapewnianej przez klasę abstrakcyjną.

Nick Holt
źródło
3
Ale użycie makiety nie sprawdzi konkretnych metod YourClass, czy się mylę? Nie tego szukam.
ripper234
1
To prawda, powyższe nie zadziała, jeśli chcesz wywołać konkretne metody klasy abstrakcyjnej.
Richard Nichols,
Przepraszam, zredaguję trochę o oczekiwaniach, które są wymagane dla każdej metody, którą wywołasz, nie tylko abstrakcyjnej.
Nick Holt
ale nadal testujesz swoją próbę, a nie konkretne metody.
Jonatan Cloutier
2

Możesz rozszerzyć klasę abstrakcyjną o anonimową klasę w teście. Na przykład (przy użyciu Junit 4):

private AbstractClassName classToTest;

@Before
public void preTestSetup()
{
    classToTest = new AbstractClassName() { };
}

// Test the AbstractClassName methods.
DwB
źródło
2

Mockito pozwala wyśmiewać klasy abstrakcyjne za pomocą @Mockadnotacji:

public abstract class My {

    public abstract boolean myAbstractMethod();

    public void myNonAbstractMethod() {
        // ...
    }
}

@RunWith(MockitoJUnitRunner.class)
public class MyTest {

    @Mock(answer = Answers.CALLS_REAL_METHODS)
    private My my;

    @Test
    private void shouldPass() {
        BDDMockito.given(my.myAbstractMethod()).willReturn(true);
        my.myNonAbstractMethod();
        // ...
    }
}

Wadą jest to, że nie można go użyć, jeśli potrzebujesz parametrów konstruktora.

Jorge Pastor
źródło
0

Możesz utworzyć anonimową klasę, wstrzyknąć sobie kpiny, a następnie przetestować tę klasę.

@RunWith(MockitoJUnitRunner.class)
public class ClassUnderTest_Test {

    private ClassUnderTest classUnderTest;

    @Mock
    MyDependencyService myDependencyService;

    @Before
    public void setUp() throws Exception {
        this.classUnderTest = getInstance();
    }

    private ClassUnderTest getInstance() {
        return new ClassUnderTest() {

            private ClassUnderTest init(
                    MyDependencyService myDependencyService
            ) {
                this.myDependencyService = myDependencyService;
                return this;
            }

            @Override
            protected void myMethodToTest() {
                return super.myMethodToTest();
            }
        }.init(myDependencyService);
    }
}

Należy pamiętać, że widoczność musi być protectedwłasnością myDependencyServiceklasy abstrakcyjnej ClassUnderTest.

Samuel
źródło
0

Whitebox.invokeMethod(..)W tym przypadku przydatne mogą być PowerMock .

Smart Coder
źródło