Verwenden von Mockito zum Testen abstrakter Klassen


213

Ich möchte eine abstrakte Klasse testen. Klar, ich kann manuell einen Mock schreiben , der von der Klasse erbt.

Kann ich dies mit einem Mocking-Framework (ich verwende Mockito) tun, anstatt mein Mock von Hand zu erstellen? Wie?


2
Ab Mockito 1.10.12 unterstützt Mockito das Ausspionieren / Verspotten abstrakter Klassen direkt:SomeAbstract spy = spy(SomeAbstract.class);
Pesche

6
Ab Mockito 2.7.14 können Sie auch abstrakte Klassen verspotten, die Konstruktorargumente erfordernmock(MyAbstractClass.class, withSettings().useConstructor(arg1, arg2).defaultAnswer(CALLS_REAL_METHODS))
Gediminas Rimsa

Antworten:


315

Mit dem folgenden Vorschlag können Sie abstrakte Klassen testen, ohne eine "echte" Unterklasse zu erstellen - der Mock ist die Unterklasse.

Verwenden Sie Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS)und verspotten Sie dann alle abstrakten Methoden, die aufgerufen werden.

Beispiel:

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

Hinweis: Die Schönheit dieser Lösung ist , dass Sie nicht haben , um die abstrakten Methoden zu implementieren, solange sie nie aufgerufen.

Meiner ehrlichen Meinung nach ist dies ordentlicher als die Verwendung eines Spions, da ein Spion eine Instanz benötigt, was bedeutet, dass Sie eine instanziierbare Unterklasse Ihrer abstrakten Klasse erstellen müssen.


14
Wie unten erwähnt, funktioniert dies nicht, wenn die abstrakte Klasse abstrakte Methoden aufruft, um getestet zu werden, was häufig der Fall ist.
Richard Nichols

11
Dies funktioniert tatsächlich, wenn die abstrakte Klasse abstrakte Methoden aufruft. Verwenden Sie einfach die Syntax doReturn oder doNothing anstelle von Mockito.when, um die abstrakten Methoden zu stubben. Wenn Sie konkrete Aufrufe stubben, stellen Sie sicher, dass das Stubben der abstrakten Aufrufe an erster Stelle steht.
Gonen I

2
Wie kann ich Abhängigkeiten in diese Art von Objekt einfügen (verspottete abstrakte Klasse, die echte Methoden aufruft)?
Samuel

2
Dies verhält sich unerwartet, wenn die betreffende Klasse Instanzinitialisierer hat. Mockito überspringt Initialisierer für Mocks, was bedeutet, dass Instanzvariablen, die inline initialisiert werden, unerwartet null sind, was zu NPEs führen kann.
Digitalbad

1
Was ist, wenn der abstrakte Klassenkonstruktor einen oder mehrere Parameter akzeptiert?
SD

68

Wenn Sie nur einige der konkreten Methoden testen müssen, ohne eines der Abstracts zu berühren, können Sie diese verwenden CALLS_REAL_METHODS(siehe Mortens Antwort ). Wenn die zu testende konkrete Methode jedoch einige der Abstracts oder nicht implementierte Schnittstellenmethoden aufruft, funktioniert dies nicht - Mockito wird sich beschweren "Kann keine echte Methode auf der Java-Schnittstelle aufrufen."

(Ja, es ist ein mieses Design, aber einige Frameworks, z. B. Tapestry 4, zwingen es Ihnen auf.)

Die Problemumgehung besteht darin, diesen Ansatz umzukehren - verwenden Sie das gewöhnliche Scheinverhalten (dh alles ist verspottet / blockiert) und doCallRealMethod()rufen Sie die zu testende konkrete Methode explizit auf. Z.B

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

Aktualisiert, um hinzuzufügen:

Für nicht leere Methoden müssen Sie thenCallRealMethod()stattdessen Folgendes verwenden , z.

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

Andernfalls beschwert sich Mockito über "Unfertiges Stubbing erkannt".


9
Dies funktioniert in einigen Fällen, Mockito ruft mit dieser Methode jedoch nicht den Konstruktor der zugrunde liegenden abstrakten Klasse auf. Dies kann dazu führen, dass die "echte Methode" aufgrund eines unerwarteten Szenarios fehlschlägt. Daher funktioniert diese Methode auch nicht in allen Fällen.
Richard Nichols

3
Ja, Sie können sich überhaupt nicht auf den Status des Objekts verlassen, sondern nur auf den Code in der aufgerufenen Methode.
David Moles

Oh, also werden die Objektmethoden vom Staat getrennt, großartig.
Haelix

17

Sie können dies mit einem Spion erreichen (verwenden Sie jedoch die neueste Version von 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()));

14

Mocking Frameworks sollen es einfacher machen, Abhängigkeiten der Klasse, die Sie testen, auszuspotten. Wenn Sie ein Verspottungsframework zum Verspotten einer Klasse verwenden, erstellen die meisten Frameworks dynamisch eine Unterklasse und ersetzen die Methodenimplementierung durch Code, um zu erkennen, wann eine Methode aufgerufen wird, und einen falschen Wert zurückzugeben.

Wenn Sie eine abstrakte Klasse testen, möchten Sie die nicht abstrakten Methoden des zu testenden Subjekts (Subject Under Test, SUT) ausführen, sodass ein Verspottungsframework nicht das ist, was Sie möchten.

Ein Teil der Verwirrung besteht darin, dass die Antwort auf die Frage, mit der Sie verlinkt haben, darin besteht, einen Schein zu erstellen, der sich von Ihrer abstrakten Klasse erstreckt. Ich würde eine solche Klasse nicht als Mock bezeichnen. Ein Mock ist eine Klasse, die als Ersatz für eine Abhängigkeit verwendet wird, mit Erwartungen programmiert ist und abgefragt werden kann, um festzustellen, ob diese Erwartungen erfüllt sind.

Stattdessen schlage ich vor, in Ihrem Test eine nicht abstrakte Unterklasse Ihrer abstrakten Klasse zu definieren. Wenn dies zu viel Code ergibt, kann dies ein Zeichen dafür sein, dass Ihre Klasse schwer zu erweitern ist.

Eine alternative Lösung wäre, Ihren Testfall selbst abstrakt zu machen, mit einer abstrakten Methode zum Erstellen des SUT (mit anderen Worten, der Testfall würde das Entwurfsmuster der Vorlagenmethode verwenden ).


8

Versuchen Sie es mit einer benutzerdefinierten Antwort.

Beispielsweise:

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

Es wird das Modell für abstrakte Methoden zurückgeben und die reale Methode für konkrete Methoden aufrufen.


5

Was mich beim Verspotten abstrakter Klassen wirklich schlecht macht, ist die Tatsache, dass weder der Standardkonstruktor YourAbstractClass () aufgerufen wird (fehlt super () im Mock), noch scheint es in Mockito eine Möglichkeit zu geben, Mock-Eigenschaften (z. B. List-Eigenschaften) standardmäßig zu initialisieren mit leerer ArrayList oder LinkedList).

Meine abstrakte Klasse (im Grunde wird der Klassenquellcode generiert) bietet KEINE Abhängigkeitssetzinjektion für Listenelemente und keinen Konstruktor, in dem die Listenelemente initialisiert werden (die ich manuell hinzugefügt habe).

Nur die Klassenattribute verwenden die Standardinitialisierung: private List dep1 = new ArrayList; private List dep2 = neue ArrayList

Es gibt also KEINE Möglichkeit, eine abstrakte Klasse zu verspotten, ohne eine Implementierung eines realen Objekts zu verwenden (z. B. Definition der inneren Klasse in einer Einheitentestklasse, Überschreiben abstrakter Methoden) und das reale Objekt auszuspionieren (was eine ordnungsgemäße Feldinitialisierung bewirkt).

Schade, dass hier nur PowerMock weiter helfen würde.


2

Angenommen, Ihre Testklassen befinden sich im selben Paket (unter einem anderen Quellstamm) wie Ihre zu testenden Klassen, können Sie einfach das Modell erstellen:

YourClass yourObject = mock(YourClass.class);

und rufen Sie die zu testenden Methoden wie jede andere Methode auf.

Sie müssen Erwartungen für jede Methode angeben, die mit den Erwartungen für konkrete Methoden aufgerufen wird, die die Supermethode aufrufen. Sie sind sich nicht sicher, wie Sie dies mit Mockito tun würden, aber ich glaube, dass dies mit EasyMock möglich ist.

Alles, was Sie tun müssen, ist, eine konkrete Instanz von zu erstellen YouClassund Ihnen den Aufwand zu ersparen, leere Implementierungen jeder abstrakten Methode bereitzustellen.

Abgesehen davon finde ich es oft nützlich, die abstrakte Klasse in meinem Test zu implementieren, wo sie als Beispielimplementierung dient, die ich über ihre öffentliche Schnittstelle teste, obwohl dies von der Funktionalität abhängt, die von der abstrakten Klasse bereitgestellt wird.


3
Aber mit dem Mock werden die konkreten Methoden von YourClass nicht getestet, oder irre ich mich? Das suche ich nicht.
Ripper234

1
Das ist richtig, das Obige funktioniert nicht, wenn Sie die konkreten Methoden für die abstrakte Klasse aufrufen möchten.
Richard Nichols

Entschuldigung, ich werde das Bit über die Erwartung bearbeiten, die für jede Methode erforderlich sind, die Sie aufrufen, nicht nur für die abstrakten.
Nick Holt

Aber dann testen Sie immer noch Ihr Modell, nicht die konkreten Methoden.
Jonatan Cloutier

2

Sie können die abstrakte Klasse in Ihrem Test um eine anonyme Klasse erweitern. Zum Beispiel (mit Junit 4):

private AbstractClassName classToTest;

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

// Test the AbstractClassName methods.

2

Mockito erlaubt das Verspotten abstrakter Klassen mittels der @MockAnnotation:

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

Der Nachteil ist, dass es nicht verwendet werden kann, wenn Sie Konstruktorparameter benötigen.


0

Sie können eine anonyme Klasse instanziieren, Ihre Mocks injizieren und diese Klasse dann testen.

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

Beachten Sie, dass die Sichtbarkeit protectedfür die Eigenschaft myDependencyServiceder abstrakten Klasse gelten muss ClassUnderTest.


0

PowerMocks Whitebox.invokeMethod(..)können in diesem Fall nützlich sein.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.