Java >> Java Tutorial >  >> Java

Einführung in Stubs

Wenn wir automatisierte Tests für unseren Code schreiben, stellen wir oft fest, dass es nicht möglich ist, die wirklichen Abhängigkeiten des zu testenden Systems aufzurufen. Das Problem könnte folgendes sein:

  • Die problematische Abhängigkeit ruft eine externe API auf, auf die von unserer Testumgebung aus nicht zugegriffen werden kann.
  • Wir können die echte Abhängigkeit nicht aufrufen, da ihr Aufruf unerwünschte Nebeneffekte verursachen würde.
  • Die echte Abhängigkeit ist zu langsam und ihr Aufruf würde unsere Testsuite verlangsamen.

Wenn wir die echte Abhängigkeit nicht verwenden können, müssen wir sie durch ein Testdouble ersetzen, das dieselbe API wie die ersetzte Abhängigkeit bereitstellen muss. Dadurch wird sichergestellt, dass das zu testende System denkt, dass es mit der Realität interagiert.

Es gibt mehrere verschiedene Testdoppel und jedes Testdoppel hilft uns, ein ganz bestimmtes Problem zu lösen. Dieses Mal werden wir uns ein Testdoppel namens Stub genauer ansehen.

Nachdem wir diesen Blogbeitrag fertiggestellt haben, werden wir:

  • Wissen Sie, was ein Stub ist.
  • Verstehen, wie ein Stub funktioniert.
  • Verstehen Sie, wann wir Stubs verwenden sollten.

Fangen wir an.

Was ist ein Stub?

Ein Stub ist ein Testdouble, das jedes Mal eine konfigurierte Antwort zurückgibt, wenn eine erwartete Interaktion zwischen dem zu testenden System und einem Stub stattfindet. Ein Stub muss folgende Anforderungen erfüllen:

  • Ein Stub muss dieselbe API bereitstellen wie die ersetzte Abhängigkeit. Das bedeutet, wenn die externe Abhängigkeit eine Klasse ist, muss unser Stub sie erweitern und alle Methoden überschreiben. Wenn die ersetzte Abhängigkeit andererseits eine Schnittstelle ist, muss unser Stub die ersetzte Schnittstelle implementieren.
  • Wir müssen in der Lage sein, die Antwort zu konfigurieren, die jedes Mal zurückgegeben wird, wenn eine erwartete Interaktion zwischen dem zu testenden System und einem Stub stattfindet. Das bedeutet, dass wir den Stub so konfigurieren können, dass er entweder ein Objekt zurückgibt oder eine Ausnahme auslöst.
  • Wenn ein unerwarteter Aufruf zwischen dem zu testenden System und einem Stub auftritt, kann ein Stub entweder eine Standardantwort (wie null , ein leerer Optional oder eine leere Sammlung) oder eine Ausnahme auslösen.
  • Ein Stub bietet keine Möglichkeit, die Interaktionen zu überprüfen, die zwischen dem zu testenden System und dem Stub stattfinden.

Als nächstes werden wir die Theorie in die Praxis umsetzen und einen neuen Stub erstellen. Beginnen wir mit einem kurzen Blick auf das zu testende System.

Einführung in das zu testende System

Das zu testende System hat eine Abhängigkeit (TodoItemRepository ), die eine Methode deklariert, die die Informationen des angeforderten Aufgabenelements aus der Datenbank abruft. Diese Methode (findById() ) nimmt die ID des angeforderten Todo-Elements als Methodenparameter und gibt einen Optional zurück Objekt, das das gefundene Aufgabenelement enthält. Wenn in der Datenbank kein Aufgabeneintrag gefunden wird, wird der findById() -Methode gibt einen leeren Optional zurück .

Der Quellcode von TodoItemRepository Die Benutzeroberfläche sieht wie folgt aus:

import java.util.Optional;

interface TodoItemRepository {

    Optional<TodoItem> findById(Long id);
}

Die TodoItem Klasse enthält die Informationen eines einzelnen ToDo-Elements. Sein Quellcode sieht wie folgt aus:

public class TodoItem {

    private Long id;
    private String title;

    //Getters an setters are omitted
}

Nehmen wir an, wir müssen Komponententests für findById() schreiben Methode des TodoItemFinderService Klasse. Diese Methode ruft einfach den findById() auf Methode des TodoItemRepository Schnittstelle und gibt den Optional zurück Objekt, das das gefundene Aufgabenelement enthält.

Der Quellcode von TodoItemFinderService Klasse sieht wie folgt aus:

import java.util.Optional;

public class TodoItemFinderService {

    private final TodoItemRepository repository;

    public TodoItemFinderService(TodoItemRepository repository) {
        this.repository = repository;
    }

    public Optional<TodoItem> findById(Long id) {
        return repository.findById(id);
    }
}

Lassen Sie uns weitermachen und herausfinden, wie wir einen einfachen TodoItemRepository erstellen können Stummel.

Erstellen eines einfachen Stubs

Wenn wir einen Stub erstellen wollen, der den echten TodoItemRepository ersetzen kann Abhängigkeit müssen wir diesen Schritten folgen:

Zuerst , müssen wir eine neue Klasse erstellen und sicherstellen, dass diese Klasse den TodoItemRepository implementiert Schnittstelle. Nachdem wir unsere Stub-Klasse erstellt haben, sieht ihr Quellcode wie folgt aus:

import java.util.Optional;

class TodoItemRepositoryStub implements TodoItemRepository {
    
    @Override
    public Optional<TodoItem> findById(Long id) {
        //Implementation left blank on purpose
    }
}

Zweiter , müssen wir einen private hinzufügen und final TodoItem Feld zum TodoItemRepositoryStub Klasse. Dieses Feld enthält den TodoItem Objekt, das von unserem Stub zurückgegeben wird, wenn das zu testende System den findById() aufruft Methode unter Verwendung der erwarteten ID.

Nachdem wir dieses Feld zur Stub-Klasse hinzugefügt haben, sieht sein Quellcode wie folgt aus:

import java.util.Optional;

class TodoItemRepositoryStub implements TodoItemRepository {

    private final TodoItem returned;
    
    @Override
    public Optional<TodoItem> findById(Long id) {
        //Implementation left blank on purpose
    }
}

Dritter , müssen wir einen Konstruktor implementieren, der es uns ermöglicht, den zurückgegebenen TodoItem zu konfigurieren Objekt. Wenn wir diesen Konstruktor implementieren, müssen wir Folgendes sicherstellen:

  • Der zurückgegebene TodoItem Objekt ist nicht null .
  • Der id des zurückgegebenen TodoItem Objekt ist nicht null .

Nachdem wir unseren Konstruktor implementiert haben, wird der Quellcode der TodoItemRepositoryStub Klasse sieht wie folgt aus:

import java.util.Optional;

class TodoItemRepositoryStub implements TodoItemRepository {

    private final TodoItem returned;

    TodoItemRepositoryStub(TodoItem returned) {
        if (returned == null) {
            throw new NullPointerException(
                    "The returned todo item cannot be null"
            );
        }

        if (returned.getId() == null) {
            throw new IllegalArgumentException(
                    "The id of the returned todo item cannot be null"
            );
        }

        this.returned = returned;
    }

    @Override
    public Optional<TodoItem> findById(Long id) {
        //Implementation left blank on purpose
    }
}

Vierter , müssen wir den findById() implementieren Methode, indem Sie diesen Schritten folgen:

  1. Wenn der Methodenaufruf erwartet wird, geben Sie einen Optional zurück Objekt, das das gefundene Aufgabenelement enthält. Ein Methodenaufruf wird erwartet, wenn id Argument ist nicht null und es ist gleich dem id des zurückgegebenen TodoItem Objekt.
  2. Wenn der Methodenaufruf unerwartet ist, werfen Sie einen neuen UnexpectedInteractionException . Ein Methodenaufruf ist unerwartet, wenn id Argument ist null oder es ist nicht gleich dem id des zurückgegebenen TodoItem Objekt.

Nachdem wir den findById() implementiert haben -Methode sieht der Quellcode unserer Stub-Klasse wie folgt aus:

import java.util.Optional;

class TodoItemRepositoryStub implements TodoItemRepository {

    private final TodoItem returned;

    TodoItemRepositoryStub(TodoItem returned) {
        if (returned == null) {
            throw new NullPointerException(
                    "The returned todo item cannot be null"
            );
        }

        if (returned.getId() == null) {
            throw new IllegalArgumentException(
                    "The id of the returned todo item cannot be null"
            );
        }

        this.returned = returned;
    }

    @Override
    public Optional<TodoItem> findById(Long id) {
        if (invocationIsExpected(id)) {
            return Optional.of(returned);
        }
        throw new UnexpectedInteractionException(
                "Unexpected method invocation. Expected that id is: %d but was: %d",
                returned.getId(),
                id
        );
    }

    private boolean invocationIsExpected(Long id) {
        return (id != null) && id.equals(returned.getId());
    }
}

Wir haben jetzt einen einfachen Stub geschrieben. Als nächstes werden wir ein paar Testmethoden schreiben, die unseren neuen Stub verwenden.

Unseren neuen Stub verwenden

Wir können unseren neuen Stub verwenden, indem wir diesen Schritten folgen:

Zuerst , müssen wir ein neues Stub-Objekt erstellen und den TodoItemRepository ersetzen Abhängigkeit des zu testenden Systems mit dem erstellten Stub. Da Stubs nicht zustandslos sind, müssen wir einen neuen Stub erstellen, bevor eine Testmethode aufgerufen wird. Mit anderen Worten, wir müssen unserer Testklasse eine neue Setup-Methode hinzufügen und diese Methode mit dem @BeforeEach annotieren Anmerkung. Nachdem wir unserer Testklasse eine neue Setup-Methode hinzugefügt haben, müssen wir sie wie folgt implementieren:

  1. Erstellen Sie einen neuen TodoItem Objekt.
  2. Erstellen Sie einen neuen TodoItemRepositoryStub Objekt und konfigurieren Sie den TodoItem Objekt, das zurückgegeben wird, wenn findById() Methode des TodoItemRepository Schnittstelle wird aufgerufen.
  3. Erstellen Sie einen neuen TodoItemFinderService Objekt und stellen Sie sicher, dass das erstellte Objekt unseren Stub verwendet.

Nachdem wir unsere Setup-Methode geschrieben haben, sieht der Quellcode unserer Testklasse wie folgt aus:

import org.junit.jupiter.api.BeforeEach;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;

class TodoItemFinderServiceTest {

    private static final Long ID = 1L;
    private static final String TITLE = "title";

    private TodoItemFinderService service;

    @BeforeEach
    void configureSystemUnderTest() {
        TodoItem found = createFoundTodoItem();
        TodoItemRepositoryStub repository = new TodoItemRepositoryStub(found);
        service = new TodoItemFinderService(repository);
    }

    private TodoItem createFoundTodoItem() {
        TodoItem found = new TodoItem();
        found.setId(ID);
        found.setTitle(TITLE);
        return found;
    }
}

Zweiter , können wir nun Testmethoden schreiben, die unseren Stub verwenden, indem wir diesen Schritten folgen:

  1. Stellen Sie sicher, dass das zu testende System einen nicht leeren Optional zurückgibt -Objekt, wenn es mit dem Argument 1L aufgerufen wird .
  2. Stellen Sie sicher, dass das zu testende System einen TodoItem zurückgibt Objekt, das den erwarteten id hat und title wenn es mit dem Argument 1L aufgerufen wird .

Nachdem wir diese Testmethoden geschrieben haben, sieht der Quellcode unserer Testklasse wie folgt aus:

import org.assertj.core.api.SoftAssertions;
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(SoftAssertionsExtension.class)
class TodoItemFinderServiceTest {

    private static final Long ID = 1L;
    private static final String TITLE = "title";

    private TodoItemFinderService service;

    @BeforeEach
    void configureSystemUnderTest() {
        TodoItem found = createFoundTodoItem();
        TodoItemRepositoryStub repository = new TodoItemRepositoryStub(found);
        service = new TodoItemFinderService(repository);
    }

    private TodoItem createFoundTodoItem() {
        TodoItem found = new TodoItem();
        found.setId(ID);
        found.setTitle(TITLE);
        return found;
    }

    @Test
    @DisplayName("Should return the found todo item")
    void shouldReturnFoundTodoItem() {
        Optional<TodoItem> result = service.findById(ID);
        assertThat(result).isPresent();
    }

    @Test
    @DisplayName("Should return the expected information of the found item")
    void shouldReturnExpectedInformationOfFoundTodoItem(SoftAssertions assertions) {
        TodoItem found = service.findById(ID).get();

        assertions.assertThat(found.getId())
                .as("id")
                .isEqualByComparingTo(ID);
        assertions.assertThat(found.getTitle())
                .as("title")
                .isEqualTo(TITLE);
    }
}

Lassen Sie uns weitermachen und die Situationen identifizieren, in denen wir Stubs verwenden sollten.

Wann sollten wir Stubs verwenden?

Ein zu testendes System kann folgende Abhängigkeiten haben:

  • Abfrageinformationen, die vom zu testenden System verwendet werden . Diese Abhängigkeiten können Informationen aus der Datenbank lesen, sie von einer externen API abrufen und so weiter.
  • Eine Aktion auslösen, die eine Nebenwirkung hat . Diese Abhängigkeiten können Informationen in der Datenbank speichern, eine HTTP-Anforderung an eine externe API senden, ein Ereignis auslösen und so weiter.
  • Hilfsfunktionen für das zu testende System bereitstellen . Diese Funktionen sind in der Regel zustandslos und verwenden keine externen Dienste wie Datenbanken oder APIs. Diese Funktionen können beispielsweise Objekte in andere Objekte umwandeln, Geschäfts- oder andere Validierungsregeln erzwingen, Informationen aus einem als Argument angegebenen Objekt parsen und so weiter.

Als nächstes werden wir diese Abhängigkeiten einzeln durchgehen und die Abhängigkeiten identifizieren, die durch Stubs ersetzt werden sollten.

Zuerst , wenn eine Abhängigkeit Informationen abfragt, die vom getesteten System verwendet werden, sollten wir sie durch einen Stub ersetzen, da die Interaktionen zwischen dem getesteten System und dieser Abhängigkeit nicht überprüft werden müssen.

Wenn wir überprüfen möchten, ob das zu testende System unseren Stub aufruft, können wir entweder Zusicherungen für das vom zu testenden System zurückgegebene Objekt schreiben, Zusicherungen für die vom zu testenden System ausgelöste Ausnahme schreiben oder sicherstellen, dass das zu testende System die verwendet Informationen, die von unserem Stub zurückgegeben werden, wenn er mit anderen Testdoubles interagiert.

Zweiter , wenn eine Abhängigkeit eine Aktion auslöst, die eine Nebenwirkung hat, müssen wir anhand der erwarteten Informationen überprüfen, ob die Aktion ausgelöst wird. Da ein Stub keine Möglichkeit bietet, die Interaktionen zwischen dem zu testenden System und dem Stub zu überprüfen, können wir diese Abhängigkeit nicht durch einen Stub ersetzen.

Dritter , wenn eine Abhängigkeit nützliche Funktionen für das zu testende System bereitstellt, sollten wir die echte Abhängigkeit verwenden, da das Stubben dieser Funktionen die Codeabdeckung unserer Tests verringert und unseren Testcode komplexer macht, als er sein könnte.

Wenn wir überprüfen möchten, ob der Systemtest diese Dienstprogrammfunktionen aufruft, können wir entweder Zusicherungen für das vom zu testenden System zurückgegebene Objekt schreiben, Zusicherungen für die vom zu testenden System ausgelöste Ausnahme schreiben oder sicherstellen, dass das zu testende System die verwendet Informationen, die von diesen Funktionen zurückgegeben werden, wenn sie mit anderen Testdoubles interagieren.

An dieser Stelle sollten wir verstehen, wie ein Stub funktioniert, und wissen, wann wir eine Abhängigkeit des zu testenden Systems durch einen Stub ersetzen sollten. Fassen wir zusammen, was wir aus diesem Blogbeitrag gelernt haben.

Zusammenfassung

Dieser Blogbeitrag hat uns vier Dinge gelehrt:

  • Ein Stub muss dieselbe API bereitstellen wie die ersetzte Abhängigkeit.
  • Ein Stub gibt jedes Mal dieselbe Antwort zurück, wenn eine erwartete Interaktion zwischen dem zu testenden System und einem Stub stattfindet.
  • Wenn ein unerwarteter Aufruf zwischen dem zu testenden System und einem Stub auftritt, muss ein Stub eine Ausnahme auslösen und eine Fehlermeldung liefern, die erklärt, warum die Ausnahme ausgelöst wurde.
  • Wir sollten eine Abhängigkeit durch einen Stub ersetzen, wenn die Abhängigkeit Informationen abfragt, die vom zu testenden System verwendet werden.

Java-Tag