[JAVA] Modèle d'objet mère et modèle de générateur de données de test

introduction

Contexte et objectif

La mise en place d'un banc d'essai est fastidieuse et difficile. En particulier, il est difficile de préparer des données de test pour une structure d'objet compliquée, et la description redondante rend le code de test bruyant et réduit la lisibilité. Cet article décrit le «modèle mère d'objet» et le «modèle de générateur de données de test», qui sont des modèles de conception typiques pour la configuration de montages de test, et présente également des idées d'amélioration.

Exemple de code

Java + JUnit 5 Sample Code est disponible sur GitHub.

Qu'est-ce qu'un banc d'essai?

Un certain état (environnement ou données) pour garantir que le même résultat est obtenu lorsque le test est exécuté de manière répétée est appelé un "dispositif de test". Cet article se concentrera sur la configuration des données nécessaires pour exécuter le test.

échantillon

Comme exemple d'explication, supposons une application de gestion de livres. Supposons que vous ayez le code de test suivant qui vérifie la méthode check qui vérifie au moment du prêt, avec le service de gestion de prêt de livres LoanService comme cible de test (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
nul Si le nombre maximum de prêts est dépassé, il ne sera pas possible de prêter.() {
        // Arrange
        Book book1 = new Book("Herbe d'oreiller");
        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
nul Si l'article prêté a une date d'expiration, un nouveau prêt ne sera pas possible.() {
        // Arrange
        Book book1 = new Book("Herbe d'oreiller");
        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"));
    }
...

Le code de test ci-dessus présente les problèmes suivants.

Le code qui configure les données de test dans la clause + Arrange est redondant et dupliqué entre les cas de test.

Comment pouvons-nous résoudre ces problèmes?

Modèle mère d'objet

Si vous souhaitez simplement éliminer la duplication de code, vous pouvez supprimer le processus de configuration des données de test en tant que méthode privée dans la classe de test, mais un code de configuration similaire est écrit dans plusieurs classes de test en double. Il y a un risque de le perdre. ʻObject Mother est une collection de méthodes d'usine qui configurent les données de test en un seul endroit. Ci-dessous se trouve une ʻObject Mother qui fournit une méthode d'usine pour configurer un ʻUser`.

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

Le cas de test ressemble à ceci:

ObjectMotherLoanServiceTest.java


    @Test
nul Si le nombre maximum de prêts est dépassé, il ne sera pas possible de prêter.() {
        // 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"));
    }

L'une des caractéristiques de ʻObject Mother` est qu'il favorise la réutilisation entre les classes de test et les cas de test en consolidant les méthodes d'usine en un seul endroit, mais le plus grand utilitaire est de nommer le modèle de données. Je pense que c'est quoi faire. Une dénomination appropriée peut améliorer la lisibilité de votre code de test. Par exemple

User user = UserMother.aUserBorrowing2Books();

Les informations bruyantes (dans ce cas de test) telles que le nom d'utilisateur, le titre du livre et la date sont éliminées du code, et la condition de test de «tout utilisateur qui emprunte deux livres» peut être lue en un coup d'œil. Je vais. D'autre part, «Object Mother» présente également les inconvénients suivants.

Modèle de générateur de données de test

Test Data Builder est une méthode pour définir des données de test à l'aide du modèle Builder qui est l'un des modèles de conception GoF. Regardons un exemple.

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

Le code de test ressemble à ceci:

TestDataBuilderLoanServiceTest.java


    @Test
nul Si l'article prêté a une date d'expiration, un nouveau prêt ne sera pas possible.() {
        // 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"));

    }

Généralement, "Test Data Builder" définit la valeur par défaut de chaque propriété, et pour créer un objet en utilisant la valeur par défaut, générez simplement Builder et appelez la méthode "build" comme indiqué ci-dessous.

new LoanBuilder().build();

S'il y a une propriété que vous souhaitez modifier par rapport à la valeur par défaut, après avoir créé le Builder, connectez-la à une chaîne de méthodes et décrivez-la, et enfin appelez la méthode build.

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

Bien que Test Data Builder améliore la visibilité de votre code de test en utilisant des valeurs par défaut et une API fluide à l'aide de chaînes de méthodes, il présente également les inconvénients suivants:

Améliorations de Test Data Builder

Sur la base de «Test Data Builder», je présenterai une technique pour améliorer ses lacunes. Tout d'abord, puisque la création d'objet avec l'opérateur new est bruyante, changez le constructeur en privé et préparez une méthode de fabrique statique à la place.

UserBuilder.java


    private UserBuilder() {}

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

En plus des valeurs par défaut, les méthodes d'usine fournissent également des modèles de configuration couramment utilisés.

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

L'écriture de code de test à l'aide de cette méthode de fabrique a pour effet de garder le code propre et la dénomination de la méthode de fabrique rend l'intention claire. C'est comme concevoir un DSL (langage spécifique au domaine).

HybridLoanServiceTest.java


    @Test
nul Si le nombre maximum de prêts est dépassé, il ne sera pas possible de prêter.() {
        // 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"));
    }

Il existe également une technique pour recevoir des rappels pour vous donner plus de flexibilité dans la configuration des objets imbriqués. L'exemple suivant est un exemple de réception d'un rappel pour configurer un objet Loan imbriqué dans un objet ʻUser à l'aide de LoanBuilder`.

UserBuilder.java


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

Un tel modèle d'implémentation est appelé le "modèle de prêt" car le côté "UserBuilder" crée un objet "LoanBuilder" et le prête au rappel reçu. * C'est une coïncidence si le mot Prêt correspond à (^ _ ^;) L'exemple de code de test à utiliser est le suivant. Les rappels sont écrits dans des expressions lambda.

HybridLoanServiceTest.java


    @Test
nul Si l'article prêté a une date d'expiration, un nouveau prêt ne sera pas possible.() {
        // 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"));
    }

Résumé

Recommended Posts

Modèle d'objet mère et modèle de générateur de données de test
Modèle de générateur de données de test ~ Améliore la maintenabilité du code de test
Modèle de constructeur
Modèle de constructeur
Modèle de conception ~ Constructeur ~
Modèle de conception (2): constructeur
String et stringbuffer et générateur de chaîne
Modèle de générateur (Java effectif)
Introduction à Effective Java en pratiquant et en apprenant (modèle Builder)