Meilleures pratiques modernes pour les tests Java

Cet article est une traduction japonaise de Modern Best Practices for Testing in Java. Traduit et publié avec la permission de l'auteur de l'article original. Je suis nouveau dans la traduction, donc je pense qu'il y a des choses étranges, mais pardonnez-moi s'il vous plaît.

Le même article est publié sur le blog Hatena. https://dhirabayashi.hatenablog.com/entry/2020/04/21/190009


Un code de test facile à maintenir et à lire est important pour établir une bonne couverture de test, ce qui vous permet de mettre en œuvre et de refactoriser de nouvelles fonctionnalités sans craindre de casser quoi que ce soit. Cet article contient de nombreuses bonnes pratiques que j'ai acquises au fil des ans en écrivant des tests unitaires et d'intégration en Java. Il comprend également des technologies modernes telles que JUnit 5, AssertJ, Testcontainers et Kotlin. Certains peuvent sembler évidents, et certains peuvent être incompatibles avec ce que vous avez lu dans les livres sur le développement et les tests de logiciels.

TL;DR

De base

Given, When, Then Le test doit être composé de trois blocs séparés par une ligne vierge. Chaque bloc de code doit être aussi court que possible. Utilisons des sous-fonctions pour raccourcir le bloc.

//Bon exemple
@Test
public void findProduct() {
    insertIntoDatabase(new Product(100, "Smartphone"));

    Product product = dao.findProduct(100);

    assertThat(product.getName()).isEqualTo("Smartphone");
}

Utilisez les préfixes «réel *» et «attendu *»

//mauvais exemple
ProductDTO product1 = requestProduct(1);

ProductDTO product2 = new ProductDTO("1", List.of(State.ACTIVE, State.REJECTED))
assertThat(product1).isEqualTo(product2);

Si vous utilisez des variables dans la même assertion de valeur, préfixez le nom de la variable avec "réel" ou "attendu". Cela facilite la lecture et clarifie l'intention de la variable. De plus, cela réduit le risque de confusion entre les valeurs attendues et mesurées.

//Bon exemple
ProductDTO actualProduct = requestProduct(1);

ProductDTO expectedProduct = new ProductDTO("1", List.of(State.ACTIVE, State.REJECTED))
assertThat(actualProduct).isEqualTo(expectedProduct); //Grand et clair

Utilisez des valeurs fixes plutôt que des valeurs aléatoires

Évitez les valeurs aléatoires car elles peuvent rendre vos tests instables, difficiles à déboguer, omettre les messages d'erreur et rendre difficile le suivi des erreurs dans votre code.

//mauvais exemple
Instant ts1 = Instant.now(); // 1557582788
Instant ts2 = ts1.plusSeconds(1); // 1557582789
int randomAmount = new Random().nextInt(500); // 232
UUID uuid = UUID.randomUUID(); // d5d1f61b-0a8b-42be-b05a-bd458bb563ad

Au lieu de cela, utilisez des valeurs fixes pour tout. Les valeurs fixes génèrent des messages d'erreur qui rendent le test plus reproductible, plus facile à déboguer et plus facile à suivre jusqu'aux lignes de code pertinentes.

//Bon exemple
Instant ts1 = Instant.ofEpochSecond(1550000001);
Instant ts2 = Instant.ofEpochSecond(1550000002);
int amount = 50;
UUID uuid = UUID.fromString("00000000-000-0000-0000-000000000001");

Vous pouvez réduire la quantité de saisie en utilisant [Use helper function](utilisez beaucoup la fonction #helper).

Rédigez un petit test clair

Faire un usage intensif des fonctions d'assistance

Extrayez du code petit ou récurrent dans des sous-fonctions et donnez-leur des noms descriptifs. Il est puissant en ce sens qu'il maintient le test court et permet d'obtenir facilement le point du test en un coup d'œil.

//mauvais exemple
@Test
public void categoryQueryParameter() throws Exception {
    List<ProductEntity> products = List.of(
            new ProductEntity().setId("1").setName("Envelope").setCategory("Office").setDescription("An Envelope").setStockAmount(1),
            new ProductEntity().setId("2").setName("Pen").setCategory("Office").setDescription("A Pen").setStockAmount(1),
            new ProductEntity().setId("3").setName("Notebook").setCategory("Hardware").setDescription("A Notebook").setStockAmount(2)
    );
    for (ProductEntity product : products) {
        template.execute(createSqlInsertStatement(product));
    }

    String responseJson = client.perform(get("/products?category=Office"))
            .andExpect(status().is(200))
            .andReturn().getResponse().getContentAsString();

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly("1", "2");
}
//Bon exemple
@Test
public void categoryQueryParameter2() throws Exception {
    insertIntoDatabase(
            createProductWithCategory("1", "Office"),
            createProductWithCategory("2", "Office"),
            createProductWithCategory("3", "Hardware")
    );

    String responseJson = requestProductsByCategory("Office");

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly("1", "2");
}
//Bon exemple(Java)
Instant ts = toInstant(1); // Instant.ofEpochSecond(1550000001)
UUID id = toUUID(1); // UUID.fromString("00000000-0000-0000-a000-000000000001")
//Bon exemple(Kotlin)
val ts = 1.toInstant()
val id = 1.toUUID()

Cette fonction d'assistance est implémentée dans Kotlin comme ceci:

fun Int.toInstant(): Instant = Instant.ofEpochSecond(this.toLong())

fun Int.toUUID(): UUID = UUID.fromString("00000000-0000-0000-a000-${this.toString().padStart(11, '0')}")

N'abusez pas des variables

L'extraction de valeurs qui sont utilisées plusieurs fois dans des variables est une pratique courante pour les développeurs.

//mauvais exemple
@Test
public void variables() throws Exception {
    String relevantCategory = "Office";
    String id1 = "4243";
    String id2 = "1123";
    String id3 = "9213";
    String irrelevantCategory = "Hardware";
    insertIntoDatabase(
            createProductWithCategory(id1, relevantCategory),
            createProductWithCategory(id2, relevantCategory),
            createProductWithCategory(id3, irrelevantCategory)
    );

    String responseJson = requestProductsByCategory(relevantCategory);

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly(id1, id2);
}

Malheureusement, cela gonfle considérablement le code de test. De plus, il est difficile de retracer la valeur jusqu'à la ligne de code appropriée à partir du message d'échec de test résultant.

Principes KISS> Principes DRY

//Bon exemple
@Test
public void variables() throws Exception {
    insertIntoDatabase(
            createProductWithCategory("4243", "Office"),
            createProductWithCategory("1123", "Office"),
            createProductWithCategory("9213", "Hardware")
    );

    String responseJson = requestProductsByCategory("Office");

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly("4243", "1123");
}

Si le code de test est court (c'est fortement recommandé), il n'y a rien de mal à savoir où la même valeur est utilisée. De plus, les méthodes sont encore plus courtes et donc plus faciles à comprendre. Et enfin, le message d'échec dans ce cas facilite la traçabilité du code.

N'étendez pas les tests existants pour "simplement tester une autre petite chose"

//mauvais exemple
public class ProductControllerTest {
    @Test
    public void happyPath() {
        //Beaucoup de code ici...
    }
}

L'ajout de tests pour des cas rares à des tests existants (de Happypath [^ happy_path]) est fascinant. [^ happy_path]: [Français] Cela semble vouloir dire quelque chose comme un système normal. Cependant, le test est vaste et difficile à comprendre. Cela rend difficile le suivi de tous les cas de test pertinents couverts par ce grand test. Ces tests sont communément appelés "tests de chemin heureux" [^ happy_path_test]. [^ happy_path_test]: [Français] Ici, la taille d'un test et la difficulté à en comprendre le contenu sont considérées comme des problèmes, mais même si vous le recherchez, le mot "happy path test" ne semble pas avoir une telle signification. Et honnêtement, je ne sais pas ce que cela signifie. Cependant, même si je ne comprends pas le sens de cette phrase, cela ne fait pas trop mal, alors je la laisse telle quelle. Si un tel test échoue, il est difficile de savoir exactement ce qui ne va pas.

//Bon exemple
public class ProductControllerTest {
    @Test
    public void multipleProductsAreReturned() {}
    @Test
    public void allProductValuesAreReturned() {}
    @Test
    public void filterByCategory() {}
    @Test
    public void filterByDateCreated() {}
}

Au lieu de cela, créez une nouvelle méthode de test avec un nom descriptif qui vous dit tout sur le comportement attendu. Oui, vous écrivez plus, mais vous pouvez créer un test clair qui correspond à votre objectif, en ne testant que le comportement pertinent. Encore une fois, les fonctions d'assistance réduisent la quantité de frappe. Et enfin, l'ajout d'un test spécialement conçu avec un nom descriptif est un excellent moyen d'enregistrer le comportement implémenté.

N'affirmez que ce que vous voulez tester

Pensez à ce que vous voulez vraiment tester. Simplement parce que vous le pouvez, évitez d'en affirmer plus que nécessaire. De plus, gardez à l'esprit ce que vous avez déjà testé lors du test précédent. Normalement, vous n'êtes pas obligé d'affirmer la même chose plusieurs fois dans chaque test. Cela permet de garder le test court, clair et non distrayant sur le comportement attendu.

Prenons un exemple. Testez les points de terminaison HTTP qui renvoient des informations sur le produit. La suite de tests doit inclure les tests suivants:

  1. Un grand "test de mappage" qui affirme que toutes les valeurs extraites de la base de données sont correctement renvoyées en tant que charge utile JSON correctement mappée au format correct. Si ʻequals () ʻest implémenté correctement, vous pouvez facilement l'affirmer en utilisant ʻisEqualTo () d'AssertJ (pour un seul élément) ou containsOnly () `(pour plusieurs éléments).
String responseJson = requestProducts();

ProductDTO expectedDTO1 = new ProductDTO("1", "evelope", new Category("office"), List.of(States.ACTIVE, States.REJECTED));
ProductDTO expectedDTO2 = new ProductDTO("2", "evelope", new Category("smartphone"), List.of(States.ACTIVE));
assertThat(toDTOs(responseJson))
        .containsOnly(expectedDTO1, expectedDTO2);
  1. Un test qui vérifie le comportement correct du paramètre de requête «? Catégorie». Nous voulons tester s'il est filtré correctement, pas si toutes les propriétés sont correctement définies. Il a déjà été testé dans le cas ci-dessus. Par conséquent, il suffit de comparer uniquement les ID de produit renvoyés.
String responseJson = requestProductsByCategory("Office");

assertThat(toDTOs(responseJson))
        .extracting(ProductDTO::getId)
        .containsOnly("1", "2");
  1. Un test qui vérifie les cas rares ou la logique métier spéciale. Par exemple, une valeur particulière de la charge utile est-elle calculée correctement? Dans ce cas, je ne suis intéressé que par certains champs JSON de la charge utile. Par conséquent, seuls les champs pertinents doivent être vérifiés pour clarifier et documenter la portée de la logique testée. Encore une fois, vous n'avez pas à réaffirmer tous les champs, car cela n'a pas d'importance ici.
assertThat(actualProduct.getPrice()).isEqualTo(100);

Test autonome

Ne pas masquer les paramètres associés (dans la fonction d'assistance)

//mauvais exemple
insertIntoDatabase(createProduct());
List<ProductDTO> actualProducts = requestProductsByCategory();
assertThat(actualProducts).containsOnly(new ProductDTO("1", "Office"));

Oui, vous devez utiliser des fonctions d'assistance pour la génération de données et les assertions. Mais vous devez les paramétrer. Définissez des paramètres pour tout ce qui est important pour votre test et qui doit être contrôlé par votre test. Ne forcez pas les lecteurs de la source à accéder à la définition de la fonction pour comprendre le contenu du test. Règle empirique: Vous devez être capable de comprendre les principaux points du test en ne regardant que la méthode de test.

//Bon exemple
insertIntoDatabase(createProduct("1", "Office"));
List<ProductDTO> actualProducts = requestProductsByCategory("Office");
assertThat(actualProducts).containsOnly(new ProductDTO("1", "Office"));

Insérer correctement les données dans la méthode de test

Tout dans la méthode de test doit être correct. Déplacer le code d'insertion de données réutilisable vers la méthode @ Before est fascinant, mais le lecteur source doit alors voler pour comprendre pleinement ce que fait le test. Ne sera pas. Encore une fois, les fonctions d'assistance qui insèrent des données aident à mettre cette tâche répétitive sur une seule ligne.

Préférez la composition à l'héritage

Ne créez pas de hiérarchies d'héritage complexes dans vos classes de test.

//mauvais exemple
class SimpleBaseTest {}
class AdvancedBaseTest extends SimpleBaseTest {}
class AllInklusiveBaseTest extends AdvancedBaseTest {}
class MyTest extends AllInklusiveBaseTest {}

Une telle hiérarchie rend la compréhension difficile et vous risquez de finir par hériter d'une classe de test de base qui contient beaucoup de choses dont vous n'avez pas besoin pour votre test actuel. Cela distrait le lecteur du code et peut provoquer des bogues. L'héritage n'est pas flexible: Il n'est pas possible d'utiliser tout ce qui est hérité de ʻAllInklusiveBaseTest, mais y a-t-il quelque chose d'héritage de sa superclasse ʻAdvancedBaseTest? [^ inheritance] De plus, les lecteurs de code doivent voler entre plusieurs classes de base pour avoir une vue d'ensemble. [^ inheritance]: [Français] Je n'ai pas compris le sens, mais il est difficile de distinguer si les membres hérités de ʻAllInklusiveBaseTest sont dérivés de ʻAllInklusiveBaseTest ou ʻAdvancedBaseTest`. Je présume qu'il est difficile de comprendre ce qui est hérité d'où la hiérarchie d'héritage est profonde. Cependant, il est possible que la phrase soit étrange en raison d'une erreur de traduction ...

"Les doublons valent mieux que les fausses abstractions" RDX in 10 Modern Software Over-Engineering Mistakes

Nous vous recommandons d'utiliser la composition à la place. Écrivez un petit extrait de code et une classe pour chaque tâche de montage particulière (démarrer la base de données de test, générer un schéma, insérer des données, démarrer un serveur Web factice). Réutilisons ces parties dans la méthode avec @ BeforeAll ou en affectant l'objet généré au champ de la classe de test. Vous construisez donc toutes les nouvelles classes de test en réutilisant ces parties. Tout comme un bloc lego. De cette façon, tous les tests ont un appareil qui vous convient parfaitement, est facile à comprendre et n'a rien à voir avec les événements. La classe de test est autonome car toutes les associations sont correctes dans la classe de test.

//Bon exemple
public class MyTest {
    //La composition au lieu de l'héritage
    private JdbcTemplate template;
    private MockWebServer taxService;

    @BeforeAll
    public void setupDatabaseSchemaAndMockWebServer() throws IOException {
        this.template = new DatabaseFixture().startDatabaseAndCreateSchema();
        this.taxService = new MockWebServer();
        taxService.start();
    }
}

//Un autre fichier
public class DatabaseFixture {
    public JdbcTemplate startDatabaseAndCreateSchema() throws IOException {
        PostgreSQLContainer db = new PostgreSQLContainer("postgres:11.2-alpine");
        db.start();
        DataSource dataSource = DataSourceBuilder.create()
                .driverClassName("org.postgresql.Driver")
                .username(db.getUsername())
                .password(db.getPassword())
                .url(db.getJdbcUrl())
                .build();
        JdbcTemplate template = new JdbcTemplate(dataSource);
        SchemaCreator.createSchema(template);
        return template;
    }
}

Répéter:

Principes KISS> Principes DRY

Le test de vidage est excellent: comparez la sortie avec des valeurs codées en dur

Ne réutilisez pas le code de production

Le test doit tester le code de production: au lieu de réutiliser le code de production. Si vous réutilisez du code de production dans vos tests, vous risquez de manquer des bogues dus au code réutilisé, car le code n'est plus testé [^ reuse_production].

//mauvais exemple
boolean isActive = true;
boolean isRejected = true;
insertIntoDatabase(new Product(1, isActive, isRejected));

ProductDTO actualDTO = requestProduct(1);

//Réutilisation du code de production
List<State> expectedStates = ProductionCode.mapBooleansToEnumList(isActive, isRejected);
assertThat(actualDTO.states).isEqualTo(expectedStates);

[^ reuse_production]: Ici, nous utilisons le code de production pour générer les valeurs attendues. Puisque nous utilisons le code de production à la fois pour la génération des valeurs attendues et pour la génération des valeurs mesurées, il est considéré comme pratiquement identique à ne rien tester.

Au lieu de cela, lorsque vous écrivez un test, pensez en termes d'entrée et de sortie. Le test définit la valeur d'entrée et compare la valeur de sortie réelle avec la valeur codée en dur. Dans la plupart des cas, vous n'avez pas besoin de réutiliser votre code.

//Bon exemple
assertThat(actualDTO.states).isEqualTo(List.of(States.ACTIVE, States.REJECTED));

N'écrivez pas la même logique que le code de production dans le test

Le code de mappage est un exemple courant de la logique réinventée dans un test. Supposons que notre test contienne une méthode mapEntityToDto () qui renvoie une valeur de retour DTO utilisée pour affirmer qu'elle contient la même valeur que l'entité insérée au début du test. Dans ce cas, il est probable que vous écrirez la même logique dans votre code de test que votre code de production. Il peut contenir des bogues.

//mauvais exemple
ProductEntity inputEntity = new ProductEntity(1, "evelope", "office", false, true, 200, 10.0);
insertIntoDatabase(input);

ProductDTO actualDTO = requestProduct(1);

 // mapEntityToDto()Contient la même logique de mappage que le code de production
ProductDTO expectedDTO = mapEntityToDto(inputEntity);
assertThat(actualDTO).isEqualTo(expectedDTO);

Encore une fois, la solution est de comparer ʻactualDTO` avec un objet de référence généré manuellement qui contient des valeurs codées en dur. C'est très simple, facile à comprendre et sans erreur.

//Bon exemple
ProductDTO expectedDTO = new ProductDTO("1", "evelope", new Category("office"), List.of(States.ACTIVE, States.REJECTED))
assertThat(actualDTO).isEqualTo(expectedDTO);

Si vous ne souhaitez pas comparer toutes les valeurs et par conséquent ne souhaitez pas créer un objet de référence complet, envisagez de comparer uniquement les sous-objets ou uniquement les valeurs associées.

N'écrivez pas trop de logique

Encore une fois, les tests concernent principalement les entrées et les sorties. Il s'agit de fournir une entrée et de comparer les valeurs mesurées et attendues. Par conséquent, vous ne devez pas et ne devez pas écrire trop de logique dans vos tests. Si vous implémentez une logique avec de nombreuses boucles et conditions, le test est plus difficile à comprendre et plus sujet aux erreurs. De plus, pour une logique d'assertion complexe, les puissantes assertions d'AssertJ peuvent faire le travail à votre place. [^ too_much_logic] [^ too_much_logic]: AssertJ le fera pour vous, vous n'aurez donc pas à travailler dur pour écrire la logique.

Test réaliste

Focus sur le test de diapositives verticales complètes

En général, il est recommandé d'utiliser un simulacre pour tester chaque classe individuellement. Mais il a de sérieux inconvénients: vous tous Le refactoring interne détruira tous les tests, car nous ne testons pas les classes de manière intégrée, mais chaque classe interne a un test. Et enfin, vous devez rédiger et maintenir divers tests. Screen Shot 2020-04-10 at 23.17.05.png

Au lieu de cela, nous vous suggérons de se concentrer sur les tests d'intégration (https://phauer.com/2019/focus-integration-tests-mock-based-tests/#integration-tests). Un "test d'intégration" est un test qui rassemble toutes les classes (comme le code de production) et parcourt toutes les couches techniques (HTTP, logique métier, base de données) sur une diapositive verticale parfaite. Cette méthode teste le comportement plutôt que l'implémentation. Ces tests sont précis, proches de la production et résistants à la refactorisation interne. Idéalement, vous n'avez besoin d'écrire qu'un seul test.

Screen Shot 2020-04-11 at 22.51.32.png * Il est recommandé de faire attention au test d'intégration (= écrire l'objet réel ensemble et tout tester en même temps) *

Il y a beaucoup plus à dire sur ce sujet. Consultez mon article de blog «Focus sur les tests d'intégration au lieu de tests simulés» pour plus d'informations. ..

N'utilisez pas de base de données en mémoire pour les tests

Screen Shot 2020-04-11 at 22.57.41.png * Si vous utilisez une base de données en mémoire, vous testerez sur une base de données différente de l'environnement de production *

Utilisez une base de données en mémoire pour les tests (H2, HSQLDB, [Fongo](https :: //github.com/fakemongo/fongo)) réduit la fiabilité et la portée des tests. La base de données en mémoire et la base de données utilisée dans l'environnement de production peuvent se comporter différemment et renvoyer des résultats différents. Par conséquent, les tests basés sur une base de données en mémoire immature ne garantissent pas le comportement correct des applications de production. De plus, vous pouvez facilement vous heurter à des situations dans lesquelles vous ne pouvez pas utiliser (ou tester) certaines fonctionnalités (spécifiques à la base de données). Cela est dû au fait que les bases de données en mémoire ne prennent pas en charge ou ne se comportent pas différemment. Pour plus d'informations à ce sujet, consultez l'article Ne pas utiliser les bases de données en mémoire pour les tests. S'il vous plaît.

La solution consiste à exécuter un test sur la base de données réelle. Heureusement, une bibliothèque appelée Testcontainers fournit une excellente API Java pour gérer les conteneurs directement dans votre code de test. Pour accélérer l'exécution, voir ici (https://phauer.com/2019/focus-integration-tests-mock-based-tests/#execution-speed).

Java/JVM

Utilisez -noverify -XX: TieredStopAtLevel = 1

Ajoutez toujours l'option JVM -noverify -XX: TieredStopAtLevel = 1 à vos paramètres d'exécution. Cela permet d'économiser 1 à 2 secondes sur le temps de démarrage de la JVM avant l'exécution du test. Ceci est particulièrement utile lors du développement initial des tests, où vous exécutez fréquemment des tests via l'EDI.

Mise à jour: à partir de Java 13, -noverify est obsolète. [^ nooverify] [^ nooverify]: Cette "mise à jour" est une traduction fidèle de "Update" dans l'article original, et était à l'origine dans l'article original. Ceci n'est pas une mise à jour de la version traduite que vous lisez.

Astuce: IntelliJ IDEA vous permet d'ajouter cet argument au modèle de configuration de lancement "JUnit", vous n'avez donc pas à ajouter un argument pour chaque nouvelle configuration d'exécution.

screenshot-idea-run-config-template-default-vm-options-marked.png

Utiliser AssertJ

AssertJ est une API sécurisée de type [^ fluent] très fluide avec une grande variété d'assertions et de messages d'erreur descriptifs. Une bibliothèque d'assertions puissante et mature. [^ fluent]: couramment. Cela signifie que vous pouvez écrire dans une chaîne de méthodes. Toutes les affirmations que vous souhaitez sont ici. Cela permet de garder votre code de test court tout en évitant d'avoir à écrire une logique d'assertion complexe avec des boucles et des conditions. Voici quelques exemples:

assertThat(actualProduct)
        .isEqualToIgnoringGivenFields(expectedProduct, "id");

assertThat(actualProductList).containsExactly(
        createProductDTO("1", "Smartphone", 250.00),
        createProductDTO("1", "Smartphone", 250.00)
);

assertThat(actualProductList)
        .usingElementComparatorIgnoringFields("id")
        .containsExactly(expectedProduct1, expectedProduct2);

assertThat(actualProductList)
        .extracting(Product::getId)
        .containsExactly("1", "2");

assertThat(actualProductList)
        .anySatisfy(product -> assertThat(product.getDateCreated()).isBetween(instant1, instant2));

assertThat(actualProductList)
        .filteredOn(product -> product.getCategory().equals("Smartphone"))
        .allSatisfy(product -> assertThat(product.isLiked()).isTrue());

Évitez ʻassertTrue () et ʻassertFalse ()

Évitez les simples assertions ʻassertTrue () et ʻassertFalse () car elles afficheront un mystérieux message d'erreur:

//mauvais exemple
assertTrue(actualProductList.contains(expectedProduct));
assertTrue(actualProductList.size() == 5);
assertTrue(actualProduct instanceof Product);
expected: <true> but was: <false>

À la place, utilisez l'assertion d'AssertJ, qui affiche un bon message d'erreur [^ out_of_the_box] sans aucune personnalisation particulière.

//Bon exemple
assertThat(actualProductList).contains(expectedProduct);
assertThat(actualProductList).hasSize(5);
assertThat(actualProduct).isInstanceOf(Product.class);
Expecting:
 <[Product[id=1, name='Samsung Galaxy']]>
to contain:
 <[Product[id=2, name='iPhone']]>
but could not find:
 <[Product[id=2, name='iPhone']]>

Si vous devez vraiment vérifier le booléen, [AssertJ's ʻas () `](http://joel-costigliola.github.io/assertj/assertj-core-" pour améliorer le message d'erreur Pensez à utiliser features-highlight.html). [^ out_of_the_box]: Le texte original est "prêt à l'emploi", ce qui signifie que vous pouvez l'utiliser avec les paramètres par défaut. Comme vous le verrez plus tard, AssertJ vous permet de personnaliser le message d'erreur en utilisant la méthode as (), mais je pense que cela signifie que vous obtiendrez un message d'erreur que vous n'avez pas à faire.

Utilisez JUnit 5

JUnit5 est une technologie de pointe pour les tests (unitaires). Il est activement développé et offre de nombreuses fonctionnalités puissantes (comme les tests paramétrés, le regroupement, les tests conditionnels, le contrôle du cycle de vie).

Utiliser un test paramétré

Les tests paramétrés permettent d'exécuter un test plusieurs fois avec des valeurs différentes. De cette façon, vous pouvez facilement tester plusieurs cas sans ajouter de code de test. JUnit 5 fournit un excellent moyen d'écrire de tels tests. @ ValueSource, @ EnumSource, @ CsvSource et @ MethodSource.

//Bon exemple
@ParameterizedTest
@ValueSource(strings = ["§ed2d", "sdf_", "123123", "§_sdf__dfww!"])
public void rejectedInvalidTokens(String invalidToken) {
    client.perform(get("/products").param("token", invalidToken))
            .andExpect(status().is(400))
}

@ParameterizedTest
@EnumSource(WorkflowState::class, mode = EnumSource.Mode.INCLUDE, names = ["FAILED", "SUCCEEDED"])
public void dontProcessWorkflowInCaseOfAFinalState(WorkflowState itemsInitialState) {
    // ...
}

Je recommande fortement de les utiliser intensivement. Parce que vous pouvez tester plus de cas avec un minimum d'effort.

Enfin, je voudrais souligner le @ CsvSource et @ MethodSource qui peuvent être utilisés pour des scénarios de test paramétrés plus avancés où vous pouvez également contrôler les valeurs attendues avec des paramètres.

@ParameterizedTest
@CsvSource({
    "1, 1, 2",
    "5, 3, 8",
    "10, -20, -10"
})
public void add(int summand1, int summand2, int expectedSum) {
    assertThat(calculator.add(summand1, summand2)).isEqualTo(expectedSum);
}

@ MethodSource est puissant lorsqu'il est utilisé en combinaison avec un objet de test dédié qui contient tous les paramètres de test pertinents et les attentes. Malheureusement, en Java, l'écriture de ces structures de données (POJO) est fastidieuse. C'est pourquoi j'utilise les classes de données de Kotlin (https://phauer.com/2018/best-practices-unit-testing-kotlin/#utilize-data-classes) pour montrer un exemple de cette fonctionnalité ci-dessous. est.

data class TestData(
    val input: String?,
    val expected: Token?
)

@ParameterizedTest
@MethodSource("validTokenProvider")
fun `parse valid tokens`(data: TestData) {
    assertThat(parse(data.input)).isEqualTo(data.expected)
}

private fun validTokenProvider() = Stream.of(
    TestData(input = "1511443755_2", expected = Token(1511443755, "2")),
    TestData(input = "151175_13521", expected = Token(151175, "13521")),
    TestData(input = "151144375_id", expected = Token(151144375, "id")),
    TestData(input = "15114437599_1", expected = Token(15114437599, "1")),
    TestData(input = null, expected = null)
)

Tests de groupe

«@ Nested» de JUnit5 est utile pour regrouper les méthodes de test. Un groupe sensible peut être un type particulier de test (tel que ʻInputIsXY, ʻErrorCases), ou chaque méthode testée dans un groupe. (GetDesign et ʻUpdateDesign`)

public class DesignControllerTest {
    @Nested
    class GetDesigns {
        @Test
        void allFieldsAreIncluded() {}
        @Test
        void limitParameter() {}
        @Test
        void filterParameter() {}
    }
    @Nested
    class DeleteDesign {
        @Test
        void designIsRemovedFromDb() {}
        @Test
        void return404OnInvalidIdParameter() {}
        @Test
        void return401IfNotAuthorized() {}
    }
}

screenshot-group-test-methods.png

Nom de test facile à lire avec @ DisplayName ou backquote Kotlin

Java utilise «@ DisplayName» de JUnit5 pour écrire des descriptions de test faciles à lire.

public class DisplayNameTest {
    @Test
    @DisplayName("Design is removed from database")
    void designIsRemoved() {}
    @Test
    @DisplayName("Return 404 in case of an invalid parameter")
    void return404() {}
    @Test
    @DisplayName("Return 401 if the request is not authorized")
    void return401() {}
}

screenshot-displayname.png

Dans Kotlin, vous pouvez écrire le nom de la méthode dans la citation arrière, et vous pouvez également inclure un espace demi-largeur. Cela facilitera la lecture tout en évitant la redondance.

@Test
fun `design is removed from db`() {}

Services à distance simulés

Afin de tester le client HTTP, vous devez vous moquer du service distant. Je préfère utiliser le serveur WebMock d'OkHttp (https://github.com/square/okhttp/tree/master/mockwebserver) pour cela.

MockWebServer serviceMock = new MockWebServer();
serviceMock.start();
HttpUrl baseUrl = serviceMock.url("/v1/");
ProductClient client = new ProductClient(baseUrl.host(), baseUrl.port());
serviceMock.enqueue(new MockResponse()
        .addHeader("Content-Type", "application/json")
        .setBody("{\"name\": \"Smartphone\"}"));

ProductDTO productDTO = client.retrieveProduct("1");

assertThat(productDTO.getName()).isEqualTo("Smartphone");

Utiliser Awaitility pour les assertions de code asynchrone

Awaitility est une bibliothèque pour tester du code asynchrone. Il est facile de définir la fréquence à laquelle vous faites des affirmations jusqu'à ce que vous échouiez finalement.

private static final ConditionFactory WAIT = await()
        .atMost(Duration.ofSeconds(6))
        .pollInterval(Duration.ofSeconds(1))
        .pollDelay(Duration.ofSeconds(1));

@Test
public void waitAndPoll(){
    triggerAsyncEvent();
    WAIT.untilAsserted(() -> {
        assertThat(findInDatabase(1).getState()).isEqualTo(State.SUCCESS);
    });
}

De cette façon, vous pouvez éviter d'utiliser l'instable Thread.sleep () dans vos tests.

Cependant, il est beaucoup plus facile de tester le code de synchronisation. C'est pourquoi vous devez [séparer l'exécution asynchrone de la logique réelle](# séparer l'exécution asynchrone de la logique réelle).

Aucune DI bootstrap requise (printemps)

(Spring) Le bootstrap du framework DI prend quelques secondes pour démarrer les tests. Cela ralentit le cycle de rétroaction, en particulier pendant le développement initial du test.

C'est pourquoi je n'utilise généralement pas DI pour les tests d'intégration. J'instancie manuellement les objets requis en appelant new et je les assemble. Si vous utilisez l'injection de constructeur, c'est assez simple. La plupart du temps, vous souhaitez tester la logique métier que vous avez écrite. Aucune DI n'est nécessaire pour cela. À titre d'exemple, consultez Mes publications sur les tests d'intégration (https://phauer.com/2019/focus-integration-tests-mock-based-tests/#integration-tests).

D'autre part, Spring Boot 2.2 introduira des fonctionnalités qui facilitent l'utilisation de l'initialisation de lazy bean, ce qui devrait considérablement accélérer les tests basés sur DI. [^ printemps] [^ spring]: publié au moment de la traduction. Probablement ceci.

Rendre la mise en œuvre testable

N'utilisez pas d'accès statique. jamais. À partir de maintenant.

L'accès statique est un anti-pattern. Premièrement, cela masque les dépendances et les effets secondaires, rend l'ensemble du code difficile à comprendre et le rend sujet aux erreurs. Deuxièmement, l'accès statique nuit à la testabilité. Vous ne pouvez plus échanger d'objets. Mais dans les tests, vous souhaitez utiliser une maquette ou un objet réel avec des paramètres différents (comme un DAO pointant vers une base de données de test).

Ainsi, au lieu d'accéder au code de manière statique, écrivez ce code dans une méthode non statique, instanciez la classe et transmettez cet objet au constructeur de l'objet requis.

//mauvais exemple
public class ProductController {
    public List<ProductDTO> getProducts() {
        List<ProductEntity> products = ProductDAO.getProducts();
        return mapToDTOs(products);
    }
}
//Bon exemple
public class ProductController {
    private ProductDAO dao;
    public ProductController(ProductDAO dao) {
        this.dao = dao;
    }
    public List<ProductDTO> getProducts() {
        List<ProductEntity> products = dao.getProducts();
        return mapToDTOs(products);
    }
}

Heureusement, les frameworks DI comme Spring offrent un moyen simple d'éviter l'accès statique. Parce qu'il gère pour nous la création et le placement de tous les objets.

Paramétrage

Testons le contrôle de toutes les parties pertinentes de la classe. Cela peut être réalisé en créant des paramètres de constructeur à partir de cet aspect.

Par exemple, supposons que votre DAO ait un nombre maximal de requêtes de 1000. Pour tester cette limite, vous devrez générer 1001 entrées de base de données dans votre test. En faisant de cette limite supérieure un paramètre de constructeur, la limite supérieure peut être définie. Dans un environnement de production, ce paramètre est 1000. Dans le test, vous pouvez définir 2. Seules trois entrées de test sont nécessaires pour tester sa fonction de plafond.

Utiliser l'injection de constructeur

L'injection de champ est mauvaise car elle est moins testable. Vous devez * must * utiliser le bootstrap de l'environnement DI dans le test, ou la magie de réflexion hacky. C'est pourquoi l'injection de constructeur est la méthode préférée. En effet, cela facilite le contrôle des objets dépendants dans vos tests.

Java nécessite quelques passe-partout.

//Bon exemple
public class ProductController {

    private ProductDAO dao;
    private TaxClient client;

    public CustomerResource(ProductDAO dao, TaxClient client) {
        this.dao = dao;
        this.client = client;
    }
}

Kotlin rend la même chose plus concise.

//Bon exemple
class ProductController(
    private val dao: ProductDAO,
    private val client: TaxClient
){
}

N'utilisez pas Instant.now () ou new Date ()

N'appelez pas ʻInstant.now () ou new Date () `dans le code de production pour obtenir l'horodatage actuel. Si vous souhaitez tester ce comportement.

//mauvais exemple
public class ProductDAO {
    public void updateDateModified(String productId) {
        Instant now = Instant.now(); // !
        Update update = Update()
            .set("dateModified", now);
        Query query = Query()
            .addCriteria(where("_id").eq(productId));
        return mongoTemplate.updateOne(query, update, ProductEntity.class);
    }
}

Le problème est que l'horodatage généré ne peut pas être contrôlé par le test. Vous ne pouvez pas affirmer la valeur exacte car elle sera toujours différente pour chaque essai. À la place, utilisez la classe Clock de Java.

//Bon exemple
public class ProductDAO {
    private Clock clock; 

    public ProductDAO(Clock clock) {
        this.clock = clock;
    }

    public void updateProductState(String productId, State state) {
        Instant now = clock.instant();
        // ...
    }
}

Pendant les tests, vous pouvez maintenant générer une maquette d'horloge et la transmettre à ProductDAO pour configurer cette maquette d'horloge pour renvoyer un horodatage fixe. Après avoir appelé ʻupdateProductState () `, il affirme si l'horodatage défini a été inséré dans la base de données.

Séparer l'exécution asynchrone de la logique réelle

Le test du code asynchrone est délicat. Les bibliothèques comme Awaitility peuvent aider, mais c'est toujours fastidieux et les tests sont encore instables. Si possible, il est judicieux de séparer la logique métier (souvent synchrone) de l'exécution asynchrone.

Par exemple, en plaçant la logique métier dans un ProductController, vous pouvez le tester avec une simple exécution synchrone. La logique asynchrone et parallèle est agrégée dans ProductScheduler et peut être testée séparément.

//Bon exemple
public class ProductScheduler {

    private ProductController controller;

    @Scheduled
    public void start() {
        CompletableFuture<String> usFuture = CompletableFuture.supplyAsync(() -> controller.doBusinessLogic(Locale.US));
        CompletableFuture<String> germanyFuture = CompletableFuture.supplyAsync(() -> controller.doBusinessLogic(Locale.GERMANY));
        String usResult = usFuture.get();
        String germanyResult = germanyFuture.get();
    }
}

Kotlin Mon article sur les Meilleures pratiques pour les tests unitaires dans Kotlin est beaucoup plus spécifique à Kotlin pour écrire des tests dans Kotlin. Contient des recommandations.

Recommended Posts

Meilleures pratiques modernes pour les tests Java
Meilleures pratiques modernes pour les tests Java
Mise à jour de l'environnement Java de Windows à l'aide de Chocolatey
Pour l'apprentissage JAVA (2018-03-16-01)
IDE 2017 pour Java
Java pour instruction
Comparez la sortie PDF en Java pour les tests d'instantanés
[Java] Package de gestion
[Java] pour instruction / étendu pour instruction
Contre-mesures pour OutOfMemoryError en java
Essayez Easy Ramdom, un outil de test PropertyBase pour Java
PNL pour Java (NLP4J) (2)
PNL pour Java (NLP4J) (1)
Exécution de débogage Java [pour les débutants Java]
Livres utilisés pour apprendre Java
Java Performance Chapter 2 Approche des tests de performances
Test de compétence Java 2018 pour les nouveaux arrivants - Principes de base-
Rationalisez les tests Java avec Spock
Java thread sans danger pour vous
Java pour les débutants, masquage des données
[Java] Conseils pour l'écriture de la source
Emplacement d'installation Java pour Mac
Application Java pour les débutants: stream
Instructions Java while et for
Que choisissent les ingénieurs Java pour le «test automatique» de leurs navigateurs? Sélénium? Séléniure? Geb?