[JAVA] Seulement ce dont je veux me souvenir, 4 modèles pour écrire des tests unitaires

J'ai aussi résumé un peu plus de convention dans "J'ai essayé d'organiser les critères de création UT en Java" ..

introduction

L'année dernière, j'ai écrit un article sur le blog intitulé "7 règles pour les tests unitaires normaux". Il y a eu plus de réponses que ce à quoi je m'attendais. Ce que j'ai écrit dans cet article n'est que le principe / principe, et certaines techniques sont nécessaires pour le réaliser.

En particulier, même si vous faites une telle règle et adhérez strictement à «écrire un test unitaire» Sans connaître la technique appropriée, la maintenance peut être difficile, la qualité peut ne pas être apportée et les déchets dont l'exécution est médiocre peuvent être produits en masse.

Ensuite, ce qu'il faut faire est de «concevoir le code original depuis le début afin que le test unitaire soit facile à écrire». alors. La première chose que vous devez apprendre n'est pas «comment écrire du code de test» mais «comment écrire du code de produit», c'est-à-dire «comment écrire du code de produit». En outre, le "depuis le début" mentionné ici est un niveau que vous pouvez faire correctement avant de le donner à des personnes telles que la révision et la fusion des relations publiques sans dire à fond "C'est le test d'abord! Faites TDD!" ..

Cependant, comme il s'agit d'un léger changement de mentalité, je pense qu'il est plus facile pour les débutants de voir un exemple concret, j'ai donc fait un exemple typique comme aide-mémoire.

Feuille de triche de conception

L'idée de base est de «séparer la logique métier et les E / S» et «renvoyer la même valeur quel que soit le nombre de répétitions». Voici un exemple concret basé sur cette idée.

Je veux créer un programme qui sort en standard

Testez une méthode qui renvoie simplement une chaîne, séparant la sortie, telle que System.out.println et Logger.info, de la partie" assembler la sortie ".

Exemple de code produit:

public class Example01Good {
    public static void main(String[] args) {
        String message = args[0];
        System.out.println(makeMessage(message));
    }

    static String makeMessage(String message) {
        return "Hello, " + message;
    }
}

Exemple de code de test:

@Test
public void testMakeMessage() {
    assertThat(Example01Good.makeMessage("world"), is("Hello, world"));
}

Je souhaite créer une méthode qui gère les nombres aléatoires

Étant donné que la valeur d'un nombre aléatoire change à chaque fois, transmettez la valeur en tant qu'argument séparément de la logique métier. Un programme qui détermine si les dés sont pairs ou impairs ressemble à ceci.

Exemple de code produit:

public class Example02Good {
    public static void main(String[] args) {
        System.out.println(check(throwDice(Math.random())));
    }

    static String check(int num) {
        return (num % 2 == 0) ? "Win" : "Lose";
    }

    static int throwDice(double rand) {
        return (int) (rand * 6);
    }
}

Exemple de code de test:

@Test
public void testCheck() {
    assertThat(Example02Good.check(1), is("Lose"));
    assertThat(Example02Good.check(2), is("Win"));
    assertThat(Example02Good.check(3), is("Lose"));
    assertThat(Example02Good.check(4), is("Win"));
    assertThat(Example02Good.check(5), is("Lose"));
    assertThat(Example02Good.check(6), is("Win"));
}

Je souhaite créer une méthode qui gère les dates comme le calcul du jour suivant

Faites-le donné de l'extérieur comme dans le cas des nombres aléatoires. Comme autre solution, comme dans l'exemple, créez une fabrique pour la génération de date et utilisez un mannequin (stub) pour les tests.

Exemple de code produit:

class SystemDate {
    public LocalDate today() {
        return LocalDate.now();
    }
}

public class Example03Good {
    SystemDate systemDate = new SystemDate();

    public LocalDate tomorrow2() {
        return systemDate.today().plusDays(1);
    }
}

Exemple de code de test:

@Test
public void testTomorrow2() {
    Example03Good target = new Example03Good();
    target.systemDate = new SystemDate() {
        @Override
        public LocalDate today() {
            return LocalDate.of(2017, 1, 16);
        }
    };
    assertThat(target.tomorrow2(), is(LocalDate.of(2017, 1, 17)));
}

Je souhaite gérer les entrées / sorties de fichiers

Fondamentalement, l'entrée / sortie de fichier doit être une méthode qui gère les chaînes de caractères, mais Dans certains cas, les données réelles sont très volumineuses ou doivent être traitées comme des "lignes". Dans ce cas, vous pouvez effectivement vérifier la lecture et l'écriture du fichier, mais comme le coût de description et le coût d'exécution sont élevés, Séparez la gestion directe des fichiers de la logique, comme Reader / Writer et InputStream / OutputStream Il est facile de programmer l'interface et d'utiliser StringReader / StringWriter etc. pour les tests.

Exemple de code produit:

public class Example04Good {
    public static void main(String[] args) throws Exception {
        System.out.println("hello");
        try (Reader reader = Files.newBufferedReader(Paths.get("intput.txt"));
                Writer writer = Files.newBufferedWriter(Paths.get("output.txt"));) {
            addLineNumber(reader, writer);
        }
    }

    static void addLineNumber(Reader reader, Writer writer) throws IOException {
        try (BufferedReader br = new BufferedReader(reader);
                PrintWriter pw = new PrintWriter(writer);) {
            int i = 1;
            for (String line = br.readLine(); line != null; line = br.readLine()) {
                pw.println(i + ": " + line);
                i += 1;
            }
        }
    }
}

Exemple de code de test:

@Test
public void testAddLineNumber() throws Exception {
    Writer writer = new StringWriter();
    addLineNumber(new StringReader("a\nb\nc\n"), writer);
    writer.flush();
    String[] actuals = writer.toString().split(System.lineSeparator());

    assertThat(actuals.length, is(3));
    assertThat(actuals[0], is("1: a"));
    assertThat(actuals[1], is("2: b"));
    assertThat(actuals[2], is("3: c"));
}

Commentaire ou idée de base

Pourquoi écrire cela, pas seulement un exemple concret? Je vais également expliquer cela.

Séparation de la logique et des E / S

Encore une fois, les bases sont la séparation de la logique et des E / S. Voici quelques conseils pour tester. Prenons "un programme qui traite les caractères reçus des arguments de ligne de commande et les sort en standard" comme exemple.

public class Example01Bad {
    public static void main(String[] args) {
        String message = args[0];
//         String message = "World"; //Pour le contrôle de fonctionnement
        System.out.println("Hello, " + message);
    }
}

N'est-ce pas le premier code que beaucoup de gens écrivent? Le code commenté est sympa. Écrivons un test unitaire pour ce programme.

@Test
public void testMain() {
    String[] args = {"world"};
    Example01Bad.main(args);
}

C'est comme ça? Il n'y a aucune affirmation! Donc, que ce programme soit normal ou non, c'est ** "les humains n'ont pas d'autre choix que de juger visuellement" ** même s'il s'agit de JUnit.

Vous pourriez penser, "Personne n'écrit ce genre de code, www.", Mais j'ai vu ce genre de "code de test qui fonctionne juste" plusieurs fois avec une logique métier plus compliquée.

Eh bien, c'est hors de question, mais une personne un peu plus consciente écrit:

/**
 *Test inutile du système hautement conscient
 */
@Test
public void testMainWithSystemOut() {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    System.setOut(new PrintStream(out));

    String[] args = {"world"};
    Example01Bad.main(args);

    assertThat(out.toString(), is("Hello, world" + System.lineSeparator()));
}

J'accroche la sortie standard et compare les résultats. Ce test répond correctement aux exigences du test, rien n'est faux, mais c'est simplement inutilement compliqué. Je vais donc simplifier les choses en modifiant le code d'origine.

public class Example01Good {
    public static void main(String[] args) {
        String message = args[0];
        System.out.println(makeMessage(message));
    }

    static String makeMessage(String message) {
        return "Hello, " + message;
    }

La "logique de traitement des chaînes de caractères" a été découpée en makeMessage. Ensuite, le test unitaire ressemble à ceci.

@Test
public void testMakeMessage() {
    assertThat(Example01Good.makeMessage("world"), is("Hello, world"));
}

C'est devenu très simple, n'est-ce pas? Ça fait du bien.

Cependant, certaines personnes peuvent penser que "je n'ai pas pu tester la fonction d'affichage des caractères à l'écran!" C'est vrai. Cependant, il n'est pas nécessaire de confirmer une telle chose par un test unitaire.

Le test unitaire le plus important sur lequel se concentrer est le test logique. Il n'est pas nécessaire d'effectuer des tests déjà bien confirmés tels que l'entrée / sortie de fichier, l'entrée / sortie standard ou la sortie de journal.

Si vous créez un tel fichier IO etc. et que sa qualité n'est pas garantie, Comme la qualité doit être garantie en testant le fichier IO créé par vous-même, il n'est pas nécessaire de le vérifier par un traitement individuel utilisant la fonction. Le test dans une telle combinaison est également important, mais il est effectué dans une autre phase telle que le test d'intégration.

Par conséquent, il est important de toujours être conscient de la création d'une logique métier qui renvoie simplement la valeur de retour en décomposant la fonction aussi petite que possible comme cette fois. Il est recommandé que les E / S et l'initialisation des valeurs incontrôlables décrites ci-dessous soient poussées vers la couche contrôleur autant que possible, et que les tests y soient supprimés de l'UT qui est exécuté chaque fois que la génération est effectuée. Pour la maintenance du code hérité, il peut être inévitable de raccorder la sortie standard comme indiqué dans le mauvais exemple, mais ce n'est pas nécessaire si vous l'écrivez vous-même.

N'initialisez pas directement les choses dont vous ne pouvez pas contrôler la valeur

Il est également important de ne pas initialiser des éléments incontrôlables tels que des dates, des nombres aléatoires ou des réseaux (comme l'appel WebAPI) dans la méthode testée, comme exemples typiques.

Par exemple, créez une méthode de demain qui demande demain.

public class Example03Bad {
    public LocalDate tomorrow() {
        return LocalDate.now().plusDays(1);
    }
}

Bien sûr, si vous initialisez LocalDate directement dans la méthode de demain comme celle-ci, la valeur changera tous les jours, donc le test automatique est impossible.

C'est un problème assez sérieux, et certaines personnes disent: «Je ne sais pas comment écrire un test unitaire», car je n'ai pas correctement éliminé ces dépendances.

La solution la plus simple consiste à passer la date en argument et à la séparer de la logique métier.

public LocalDate tomorrow(LocalDate today) {
    return today.plusDays(1);
}

Ensuite, le test peut être écrit avec une valeur fixe comme celle-ci.

@Test
public void testTomorrow() {
    Example03Good target = new Example03Good();
    assertThat(target.tomorrow(LocalDate.of(2017, 1, 16)), is(LocalDate.of(2017, 1, 17)));
}

En gros, c'est bien, mais il est parfois plus facile d'utiliser le modèle et les stubs de la méthode d'usine quand ils sont nombreux. Tout d'abord, créez une classe SystemDate qui génère LocalDate, utilisez LocalDate.now dedans et définissez-la dans le champ Example03Good. La logique métier obtient le LocalDate via la classe SystemDate, ce qui est retourné dépend donc de l'implémentation. Le code produit est LocalDate.now.

class SystemDate {
    public LocalDate today() {
        return LocalDate.now();
    }
}

public class Example03Good {
    SystemDate systemDate = new SystemDate();

    public LocalDate tomorrow2() {
        return systemDate.today().plusDays(1);
    }
}

Le code de test écrase le systemDate avec un stub comme celui-ci.

@Test
public void testTomorrow2() {
    Example03Good target = new Example03Good();
    target.systemDate = new SystemDate() {
        @Override
        public LocalDate today() {
            return LocalDate.of(2017, 1, 16);
        }
    };
    assertThat(target.tomorrow2(), is(LocalDate.of(2017, 1, 17)));
}

L'astuce ici est de placer les champs Factory tels que SystemDate au niveau du package ou supérieur, et non privés. Ensuite, vous pouvez changer d'implémentation sans utiliser de conteneur DI ou de framework Mock. Bien sûr, il est normal de le rendre privé et de le passer dans le constructeur, ou de le placer dans le setter, mais à la fin, il est toujours possible de le changer, il vaut donc mieux être facile.

Résumé

Eh bien, je l'ai écrit brièvement, mais comment est-ce? J'espère que vous avez trouvé qu'un peu d'ingéniosité de conception rend le test beaucoup plus facile à écrire.

Ce n'est pas difficile, mais je fais souvent la même remarque aux débutants, et je ne connaissais pas le matériel qui résume de ce point de vue, alors je l'ai écrit. D'ailleurs, on dit que «le TDD (développement piloté par les tests) contribue à la qualité» car ce type d'écriture est naturellement forcé.

De plus, lors de l'étude d'un langage fonctionnel, certains points semblent être le summum de ces méthodes de conception, je pense donc que de nombreux points peuvent être utiles.

Alors cette année aussi Happy Hacking!

Recommended Posts

Seulement ce dont je veux me souvenir, 4 modèles pour écrire des tests unitaires
Je veux écrire un test unitaire!
[Rails] Comment implémenter un test unitaire d'un modèle
Je souhaite appliquer ContainerRelativeShape uniquement à des coins spécifiques [SwiftUI]
Je souhaite créer une annotation générique pour un type
Je souhaite générer des informations de manière aléatoire lors de l'écriture du code de test
Je souhaite renvoyer plusieurs valeurs de retour pour l'argument saisi
Je veux convertir des caractères ...
Je souhaite rechercher de manière récursive des fichiers dans un répertoire spécifique
Je souhaite accorder des autorisations de modification et de suppression uniquement à l'affiche
L'histoire de Collectors.groupingBy que je veux garder pour la postérité
[Ruby] Je souhaite afficher uniquement le caractère impair dans la chaîne de caractères
Lorsque vous souhaitez que Rails désactive une session pour un contrôleur spécifique
Liste de réglages de Glassfish que je souhaite conserver pour le moment
[Ruby] Je souhaite extraire uniquement la valeur du hachage et uniquement la clé
[Rspec] Flux de l'introduction de Rspec à l'écriture du code de test unitaire pour le modèle
Je veux que vous utilisiez Enum # name () pour la clé de SharedPreference