Mockitos Java 8-Funktionen

Java 8-Funktionen von Mockito

1. Überblick

Java 8 führte eine Reihe neuer, fantastischer Funktionen ein, wie Lambda und Streams. Und natürlich nutzte Mockito diese jüngsten Innovationen in2nd major version.

In diesem Artikel werden wir alles erforschen, was diese leistungsstarke Kombination zu bieten hat.

2. Verspottende Schnittstelle mit einer Standardmethode

Ab Java 8 können wir nun Methodenimplementierungen in unsere Schnittstellen schreiben. Dies mag eine großartige neue Funktionalität sein, aber ihre Einführung in die Sprache verstieß gegen ein starkes Konzept, das seit seiner Konzeption Teil von Java war.

Mockito Version 1 war für diese Änderung nicht bereit. Grundsätzlich, weil es uns nicht erlaubt hat, echte Methoden von Schnittstellen aufzurufen.

Stellen Sie sich vor, wir haben eine Schnittstelle mit zwei Methodendeklarationen: Die erste ist die altmodische Methodensignatur, an die wir alle gewöhnt sind, und die andere ist eine brandneuedefault-Methode:

public interface JobService {

    Optional findCurrentJobPosition(Person person);

    default boolean assignJobPosition(Person person, JobPosition jobPosition) {
        if(!findCurrentJobPosition(person).isPresent()) {
            person.setCurrentJobPosition(jobPosition);

            return true;
        } else {
            return false;
        }
    }
}

Beachten Sie, dass dieassignJobPosition()default-Methode die nicht implementiertefindCurrentJobPosition()-Methode aufruft.

Angenommen, wir möchten unsere Implementierung vonassignJobPosition() testen, ohne eine tatsächliche Implementierung vonfindCurrentJobPosition() zu schreiben. Wir könnten einfach eine verspottete Version vonJobService, erstellen und dann Mockito anweisen, einen bekannten Wert aus dem Aufruf an unsere nicht implementierte Methode zurückzugeben und die reale Methode aufzurufen, wennassignJobPosition() aufgerufen wird:

public class JobServiceUnitTest {

    @Mock
    private JobService jobService;

    @Test
    public void givenDefaultMethod_whenCallRealMethod_thenNoExceptionIsRaised() {
        Person person = new Person();

        when(jobService.findCurrentJobPosition(person))
              .thenReturn(Optional.of(new JobPosition()));

        doCallRealMethod().when(jobService)
          .assignJobPosition(
            Mockito.any(Person.class),
            Mockito.any(JobPosition.class)
        );

        assertFalse(jobService.assignJobPosition(person, new JobPosition()));
    }
}

Dies ist absolut vernünftig und würde gut funktionieren, vorausgesetzt, wir verwenden eine abstrakte Klasse anstelle einer Schnittstelle.

Das Innenleben von Mockito 1 war jedoch für diese Struktur noch nicht bereit. Wenn wir diesen Code mit Mockito vor Version 2 ausführen würden, bekämen wir diesen gut beschriebenen Fehler:

org.mockito.exceptions.base.MockitoException:
Cannot call a real method on java interface. The interface does not have any implementation!
Calling real methods is only possible when mocking concrete classes.

Mockito macht seinen Job und teilt uns mit, dass es keine echten Methoden für Schnittstellen aufrufen kann, da dieser Vorgang vor Java 8 undenkbar war.

Die gute Nachricht ist, dass wir diesen Fehler beheben können, indem wir nur die Version von Mockito ändern, die wir verwenden. Mit Maven könnten wir beispielsweise Version 2.7.5 verwenden (die neueste Mockito-Version finden Sie unterhere):


    org.mockito
    mockito-core
    2.7.5
    test

Der Code muss nicht geändert werden. Bei der nächsten Testdurchführung tritt der Fehler nicht mehr auf.

3. Standardwerte fürOptional undStream zurückgeben

Optional undStream sind weitere neue Java 8-Ergänzungen. Eine Ähnlichkeit zwischen den beiden Klassen besteht darin, dass beide einen speziellen Werttyp haben, der ein leeres Objekt darstellt. Dieses leere Objekt erleichtert die Vermeidung der bisher allgegenwärtigenNullPointerException.

3.1. Beispiel mitOptional

Stellen Sie sich einen Dienst vor, der die im vorherigen Abschnitt beschriebenenJobService einfügt und über eine Methode verfügt, dieJobService#findCurrentJobPosition() aufruft:

public class UnemploymentServiceImpl implements UnemploymentService {

    private JobService jobService;

    public UnemploymentServiceImpl(JobService jobService) {
        this.jobService = jobService;
    }

    @Override
    public boolean personIsEntitledToUnemploymentSupport(Person person) {
        Optional optional = jobService.findCurrentJobPosition(person);

        return !optional.isPresent();
    }
}

Angenommen, wir möchten einen Test erstellen, um zu überprüfen, ob eine Person, die keine aktuelle Arbeitsposition hat, Anspruch auf Arbeitslosenunterstützung hat.

In diesem Fall würden wirfindCurrentJobPosition() zwingen, ein leeresOptional zurückzugeben. Before Mockito 2 mussten wir den Aufruf dieser Methode verspotten:

public class UnemploymentServiceImplUnitTest {

    @Mock
    private JobService jobService;

    @InjectMocks
    private UnemploymentServiceImpl unemploymentService;

    @Test
    public void givenReturnIsOfTypeOptional_whenMocked_thenValueIsEmpty() {
        Person person = new Person();

        when(jobService.findCurrentJobPosition(any(Person.class)))
          .thenReturn(Optional.empty());

        assertTrue(unemploymentService.personIsEntitledToUnemploymentSupport(person));
    }
}

Diese Anweisungwhen(…).thenReturn(…)in Zeile 13 ist erforderlich, da Mockitos Standardrückgabewert für alle Methodenaufrufe an ein verspottetes Objektnull ist. Version 2 hat dieses Verhalten geändert.

Da wir beim Umgang mitOptional,Mockito now returns an empty Optional by default selten mit Nullwerten umgehen. Dies ist genau der gleiche Wert wie die Rückgabe eines Aufrufs anOptional.empty().

Also,when using Mockito version 2, wir könnten Zeile 13 loswerden und unser Test wäre immer noch erfolgreich:

public class UnemploymentServiceImplUnitTest {

    @Test
    public void givenReturnIsOptional_whenDefaultValueIsReturned_thenValueIsEmpty() {
        Person person = new Person();

        assertTrue(unemploymentService.personIsEntitledToUnemploymentSupport(person));
    }
}

3.2. Beispiel mitStream

Das gleiche Verhalten tritt auf, wenn wir eine Methode verspotten, dieStream zurückgibt.

Fügen wir unsererJobService-Schnittstelle eine neue Methode hinzu, die einen Stream zurückgibt, der alle Jobpositionen darstellt, an denen eine Person jemals gearbeitet hat:

public interface JobService {
    Stream listJobs(Person person);
}

Diese Methode wird für eine andere neue Methode verwendet, mit der abgefragt wird, ob eine Person jemals an einem Job gearbeitet hat, der einer bestimmten Suchzeichenfolge entspricht:

public class UnemploymentServiceImpl implements UnemploymentService {

    @Override
    public Optional searchJob(Person person, String searchString) {
        return jobService.listJobs(person)
          .filter((j) -> j.getTitle().contains(searchString))
          .findFirst();
    }
}

Nehmen wir also an, wir möchten die Implementierung vonsearchJob(), ordnungsgemäß testen, ohne uns Gedanken über das Schreiben vonlistJobs() machen zu müssen, und nehmen wir an, dass wir das Szenario testen möchten, wenn die Person noch keine Arbeit erledigt hat. In diesem Fall möchten wir, dasslistJobs() ein leeresStream zurückgibt.

Before Mockito 2, we would need to mock the call to listJobs(), um einen solchen Test zu schreiben:

public class UnemploymentServiceImplUnitTest {

    @Test
    public void givenReturnIsOfTypeStream_whenMocked_thenValueIsEmpty() {
        Person person = new Person();
        when(jobService.listJobs(any(Person.class))).thenReturn(Stream.empty());

        assertFalse(unemploymentService.searchJob(person, "").isPresent());
    }
}

If we upgrade to version 2, wir könnten den Aufruf vonwhen(…).thenReturn(…) abbrechen, weil jetztMockito will return an empty Stream on mocked methods by default:

public class UnemploymentServiceImplUnitTest {

    @Test
    public void givenReturnIsStream_whenDefaultValueIsReturned_thenValueIsEmpty() {
        Person person = new Person();

        assertFalse(unemploymentService.searchJob(person, "").isPresent());
    }
}

4. Nutzung von Lambda-Ausdrücken

Mit den Lambda-Ausdrücken von Java 8 können wir Anweisungen viel kompakter und leichter lesbar machen. Bei der Arbeit mit Mockito sind 2 sehr schöne Beispiele für die Einfachheit, die Lambda-Ausdrücke bringen,ArgumentMatchers und benutzerdefinierteAnswers.

4.1. Kombination von Lambda undArgumentMatcher

Vor Java 8 mussten wir eine Klasse erstellen, dieArgumentMatcher implementiert, und unsere benutzerdefinierte Regel in die Methodematches() schreiben.

Mit Java 8 können wir die innere Klasse durch einen einfachen Lambda-Ausdruck ersetzen:

public class ArgumentMatcherWithLambdaUnitTest {

    @Test
    public void whenPersonWithJob_thenIsNotEntitled() {
        Person peter = new Person("Peter");
        Person linda = new Person("Linda");

        JobPosition teacher = new JobPosition("Teacher");

        when(jobService.findCurrentJobPosition(
          ArgumentMatchers.argThat(p -> p.getName().equals("Peter"))))
          .thenReturn(Optional.of(teacher));

        assertTrue(unemploymentService.personIsEntitledToUnemploymentSupport(linda));
        assertFalse(unemploymentService.personIsEntitledToUnemploymentSupport(peter));
    }
}

4.2. Kombination von Lambda und CustomAnswer

Der gleiche Effekt kann erzielt werden, wenn Lambda-Ausdrücke mit MockitosAnswerkombiniert werden.

Wenn wir beispielsweise Aufrufe derlistJobs()-Methode simulieren wollten, um einStream mit einem einzelnenJobPosition zurückzugeben, wenn der Name desPerson „Peter ”Und andernfalls ein leeresStream, müssten wir eine Klasse (anonym oder inner) erstellen, die dieAnswer-Schnittstelle implementiert.

Die Verwendung eines Lambda-Ausdrucks ermöglicht es uns, das gesamte Scheinverhalten inline zu schreiben:

public class CustomAnswerWithLambdaUnitTest {

    @Before
    public void init() {
        MockitoAnnotations.initMocks(this);

        when(jobService.listJobs(any(Person.class))).then((i) ->
          Stream.of(new JobPosition("Teacher"))
          .filter(p -> ((Person) i.getArgument(0)).getName().equals("Peter")));
    }
}

Beachten Sie, dass in der obigen Implementierung die innere Klasse vonPersonAnswernicht erforderlich ist.

5. Fazit

In diesem Artikel wurde erläutert, wie Sie die neuen Funktionen von Java 8 und Mockito 2 zusammen nutzen können, um saubereren, einfacheren und kürzeren Code zu schreiben. Wenn Sie mit einigen der Java 8-Funktionen, die wir hier gesehen haben, nicht vertraut sind, lesen Sie einige unserer Artikel:

Überprüfen Sie auch den zugehörigen Code auf unserenGitHub repository.