[JAVA] Objektmuttermuster und Test Data Builder-Muster

Einführung

Hintergrund und Zweck

Das Einrichten einer Testvorrichtung ist mühsam und schwierig. Insbesondere ist es mühsam, Testdaten für eine komplizierte Objektstruktur vorzubereiten, und die redundante Beschreibung macht den Testcode verrauscht und verringert die Lesbarkeit. Dieser Artikel beschreibt das "Objektmuttermuster" und das "Test Data Builder-Muster", die typische Entwurfsmuster zum Einrichten von Testvorrichtungen sind, und stellt auch Verbesserungsvorschläge vor.

Beispielcode

Java + JUnit 5 Beispielcode ist auf GitHub verfügbar.

Was ist eine Testvorrichtung?

Ein bestimmter Zustand (Umgebung oder Daten), um sicherzustellen, dass das gleiche Ergebnis erzielt wird, wenn der Test wiederholt ausgeführt wird, wird als "Testvorrichtung" bezeichnet. Dieser Beitrag konzentriert sich auf das Einrichten der Daten, die zum Ausführen des Tests erforderlich sind.

Stichprobe

Nehmen Sie als Beispiel für eine Erklärung eine Buchverwaltungsanwendung an. Angenommen, Sie haben den folgenden Testcode, der die "check" -Methode überprüft, mit der der Kreditverwaltungsdienst "LoanService" als Testziel überprüft wird (SUT: System Under Test).

SimpleLoanServiceTest.java


public class SimpleLoanServiceTest {

    private static final LocalDate TODAY = LocalDate.now();
    private static final LocalDate YESTERDAY = TODAY.minusDays(1);
    private static final LocalDate A_WEEK_AGO = TODAY.minusDays(7);

    private LoanService sut;

    @BeforeEach
    void setup() {
        sut = new LoanService(TODAY);
    }
...
    @Test
nichtig Wenn die maximale Anzahl von Krediten überschritten wird, ist eine Kreditvergabe nicht möglich.() {
        // Arrange
        Book book1 = new Book("Kissen Gras");
        Loan loan1 = new Loan(book1, A_WEEK_AGO, TODAY);
        Book book2 = new Book("Genji Monogatari");
        Loan loan2 = new Loan(book2, A_WEEK_AGO, TODAY);

        Loans loans = Loans.of(loan1, loan2);

        User user = new User("Yamamoto", TODAY, loans);

        Book[] books = new Book[]{new Book("Taketori Monogatari"), new Book("Heike Monogatari")};

        // Act
        LoanCheckResult result = sut.check(user, books);

        // Assert
        assertThat(result.hasError, is(true));
        assertThat(result.errorCode, is("LIMIT_EXCEEDED"));
    }

    @Test
ungültig Wenn der ausgeliehene Gegenstand ein Ablaufdatum hat, ist ein neuer Kredit nicht möglich.() {
        // Arrange
        Book book1 = new Book("Kissen Gras");
        Loan loan1 = new Loan(book1, A_WEEK_AGO, TODAY);
        Book book2 = new Book("Genji Monogatari");
        Loan loan2 = new Loan(book2, A_WEEK_AGO, YESTERDAY);

        Loans loans = Loans.of(loan1, loan2);

        User user = new User("Yamamoto", TODAY, loans);

        Book book = new Book("Taketori Monogatari");

        // Act
        LoanCheckResult result = sut.check(user, book);

        // Assert
        assertThat(result.hasError, is(true));
        assertThat(result.errorCode, is("UNRETURNED_BOOKS"));
    }
...

Der obige Testcode weist die folgenden Probleme auf.

Der Code, der Testdaten in der + Arrange-Klausel einrichtet, ist redundant und wird zwischen Testfällen dupliziert.

Wie können wir diese Probleme lösen?

Objekt Mutter Muster

Wenn Sie lediglich die Codeduplizierung beseitigen möchten, können Sie den Testdaten-Setup-Prozess als private Methode in der Testklasse ausschneiden. Ein ähnlicher Setup-Code wird jedoch in mehreren Testklassen doppelt geschrieben. Es besteht die Gefahr, dass es verloren geht. Object Mother ist eine Sammlung von Factory-Methoden, mit denen Testdaten an einem Ort eingerichtet werden. Unten finden Sie eine "Objektmutter", die eine Factory-Methode zum Einrichten eines "Benutzers" bietet.

UserMother.java


public class UserMother {

    public static User aUserWithoutLoans() {
        Loans loans = Loans.empty();
        return new User("USER", LocalDate.MAX, loans);
    }

    public static User aUserBorrowing2Books() {
        Loans loans = Loans.of(aLoan(), aLoan());
        return new User("USER", LocalDate.MAX, loans);
    }

    public static User aUserBorrowing3Books() {
        Loans loans = Loans.of(aLoan(), aLoan(), aLoan());
        return new User("USER", LocalDate.MAX, loans);
    }
...

Der Testfall sieht folgendermaßen aus:

ObjectMotherLoanServiceTest.java


    @Test
nichtig Wenn die maximale Anzahl von Krediten überschritten wird, ist eine Kreditvergabe nicht möglich.() {
        // Arrange
        User user = UserMother.aUserBorrowing2Books();

        Book[] books = BookMother.books(2);

        // Act
        LoanCheckResult result = sut.check(user, books);

        // Assert
        assertThat(result.hasError, is(true));
        assertThat(result.errorCode, is("LIMIT_EXCEEDED"));
    }

Eines der Merkmale von "Object Mother" ist, dass es die Wiederverwendung über Testklassen und Testfälle hinweg fördert, indem Factory-Methoden an einem Ort konsolidiert werden. Der größte Nutzen besteht jedoch darin, das Datenmuster zu benennen. Ich denke, es ist was zu tun ist. Die richtige Benennung kann die Lesbarkeit Ihres Testcodes verbessern. Zum Beispiel

User user = UserMother.aUserBorrowing2Books();

Lautstarke Informationen (in diesem Testfall) wie Benutzername, Buchtitel und Datum werden aus dem Code entfernt, und die Testbedingung "Jeder Benutzer, der zwei Bücher ausleiht" kann auf einen Blick gelesen werden. Ich werde. Andererseits hat "Objektmutter" auch die folgenden Nachteile.

Test Data Builder-Muster

Test Data Builder ist eine Methode zum Festlegen von Testdaten mithilfe von Builder-Muster, einem der GoF-Entwurfsmuster. Schauen wir uns ein Beispiel an.

UserBuilder.java


public class UserBuilder {

    private String name = "DEFAULT_NAME";
    private LocalDate expirationDate = LocalDate.MAX;
    private Loans loans = Loans.empty();

    public UserBuilder withLoans(Loans loans) {
        this.loans = loans;
        return this;
    }

    public UserBuilder withExpirationDate(LocalDate expirationDate) {
        this.expirationDate = expirationDate;
        return this;
    }

    public User build() {
        return new User(name, expirationDate, loans);
    }
}

Der Testcode sieht folgendermaßen aus:

TestDataBuilderLoanServiceTest.java


    @Test
ungültig Wenn der ausgeliehene Gegenstand ein Ablaufdatum hat, ist ein neuer Kredit nicht möglich.() {
        // Arrange
        Loan loan1 = new LoanBuilder().build();
        Loan loan2 = new LoanBuilder().withDueDate(YESTERDAY).build();

        Loans loans = Loans.of(loan1, loan2);

        User user = new UserBuilder().withLoans(loans).build();

        Book book = new BookBuilder().build();

        // Act
        LoanCheckResult result = sut.check(user, book);

        // Assert
        assertThat(result.hasError, is(true));
        assertThat(result.errorCode, is("UNRETURNED_BOOKS"));

    }

Im Allgemeinen definiert "Test Data Builder" den Standardwert jeder Eigenschaft. Um ein Objekt mit dem Standardwert zu erstellen, generieren Sie einfach den Builder und rufen Sie die "build" -Methode wie unten gezeigt auf.

new LoanBuilder().build();

Wenn es eine Eigenschaft gibt, die Sie gegenüber dem Standardwert ändern möchten, verbinden Sie sie nach dem Erstellen des Builders mit einer Methodenkette, beschreiben Sie sie und rufen Sie schließlich die Methode "build" auf.

User user = new UserBuilder().withLoans(loans).build();

Während "Test Data Builder" die Sichtbarkeit Ihres Testcodes durch Verwendung von Standardwerten und "fließende API" mithilfe von Methodenketten verbessert, weist es auch die folgenden Nachteile auf:

Test Data Builder-Verbesserungen

Basierend auf Test Data Builder werde ich eine Technik einführen, um seine Mängel zu verbessern. Da die Objekterstellung mit dem neuen Operator verrauscht ist, ändern Sie zunächst den Konstruktor in privat und bereiten Sie stattdessen eine statische Factory-Methode vor.

UserBuilder.java


    private UserBuilder() {}

    public static UserBuilder ofDefault() {
        return new UserBuilder();
    }

Zusätzlich zu den Standardwerten bieten die Factory-Methoden auch häufig verwendete Setup-Muster.

UserBuilder.java


    public static UserBuilder borrowing(int numOfBooks) {
        UserBuilder builder = new UserBuilder();
        List<Loan> loanList = new ArrayList<>();
        for (int i = 0; i < numOfBooks; i++) {
            Loan loan = LoanBuilder.ofDefault().build();
            loanList.add(loan);
        }
        builder.loans = Loans.of(loanList.toArray(new Loan[0]));
        return builder;
    }

Das Schreiben von Testcode mit dieser Factory-Methode bewirkt, dass der Code sauber bleibt, und die Benennung der Factory-Methode macht die Absicht klar. Es ist wie beim Entwerfen einer DSL (domänenspezifischen Sprache).

HybridLoanServiceTest.java


    @Test
nichtig Wenn die maximale Anzahl von Krediten überschritten wird, ist eine Kreditvergabe nicht möglich.() {
        // Arrange
        User user = UserBuilder.borrowing(2).build();

        Book[] books = BookBuilder.ofDefault().build(2);

        // Act
        LoanCheckResult result = sut.check(user, books);

        // Assert
        assertThat(result.hasError, is(true));
        assertThat(result.errorCode, is("LIMIT_EXCEEDED"));
    }

Es gibt auch eine Technik zum Empfangen von Rückrufen, um Ihnen mehr Flexibilität beim Einrichten verschachtelter Objekte zu bieten. Das folgende Beispiel ist ein Beispiel für den Empfang eines Rückrufs zum Einrichten eines in einem Benutzerobjekt verschachtelten "Loan" -Objekts mit "LoanBuilder".

UserBuilder.java


   public UserBuilder borrowing(Consumer<LoanBuilder> callback) {
        LoanBuilder loanBuilder = LoanBuilder.ofDefault();
        callback.accept(loanBuilder);
        loans.add(loanBuilder.build());
        return this;
    }

Ein solches Implementierungsmuster wird als "Kreditmuster" bezeichnet, da die "UserBuilder" -Seite ein "LoanBuilder" -Objekt erstellt und es dem empfangenen Rückruf verleiht. * Es ist ein Zufall, dass das Wort "Darlehen" übereinstimmt (^ _ ^;) Das zu verwendende Testcodebeispiel lautet wie folgt. Rückrufe werden in Lambda-Ausdrücken geschrieben.

HybridLoanServiceTest.java


    @Test
ungültig Wenn der ausgeliehene Gegenstand ein Ablaufdatum hat, ist ein neuer Kredit nicht möglich.() {
        // Arrange
        User user = UserBuilder.ofDefault()
                    .borrowing(loanBuilder -> loanBuilder.noop())
                    .borrowing(loanBuilder -> loanBuilder.withDueDate(YESTERDAY))
                    .build();

        Book book = BookBuilder.ofDefault().build();

        // Act
        LoanCheckResult result = sut.check(user, book);

        // Assert
        assertThat(result.hasError, is(true));
        assertThat(result.errorCode, is("UNRETURNED_BOOKS"));
    }

Zusammenfassung

Recommended Posts

Objektmuttermuster und Test Data Builder-Muster
Test Data Builder-Muster ~ Verbessert die Wartbarkeit des Testcodes
Builder-Muster
Builder-Muster
Entwurfsmuster ~ Builder ~
Entwurfsmuster (2): Builder
String und Stringbuffer und String Builder
Builder-Muster (effektives Java)
Einführung in effektives Java durch Üben und Lernen (Builder-Muster)