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
En faisant un usage intensif des fonctions d'assistance, des tests paramétrés et des puissantes assertions AssertJ, en ne surutilisant pas les variables, en vérifiant uniquement la pertinence et en évitant d'écrire des tests pour de rares cas. ** Rédigez un petit test clair. ** **
Rédigez un test autonome en clarifiant tous les paramètres pertinents, en insérant correctement les données et en utilisant la composition plutôt que l'héritage. ** **
Rédigez un ** test de vidage [^ dumptest] en évitant la réutilisation du code de production et en vous concentrant sur la comparaison des valeurs de sortie avec des valeurs codées en dur. ** ** [^ dumptest]: Je n'étais pas sûr de ce qu'était le test, mais cela ressemble à un test qui code en dur les valeurs attendues.
Principes KISS> Principes DRY
Concentrez-vous sur les tests de [^ diapositives] sur des diapositives verticales complètes, évitez d'utiliser des bases de données en mémoire et ** écrivez des tests proches de la production. ** ** [^ diapo]: Cela semble être un test d'intégration.
JUnit 5 et Assert J sont de très bons choix.
Évitez les accès statiques, utilisez l'injection de constructeur, utilisez Clock
[^ clock], et séparez votre logique métier de l'exécution asynchrone, et travaillez dur pour rendre votre implémentation facile à tester.
[^ clock]: la classe java.time.Clock.
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");
}
//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
É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).
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");
}
createProductWithCategory ()
) et pour des assertions complexes. Ne transmettez aux fonctions d'assistance que les paramètres pertinents pour votre test. Pour les autres valeurs, utilisez les valeurs par défaut appropriées. Dans Kotlin, c'est facile à faire avec les arguments par défaut. En Java, vous devez utiliser des chaînes de méthodes et des surcharges pour obtenir des arguments pseudo-par défautìnsertIntoDatabase ()
)//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')}")
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.
//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.
//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é.
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:
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);
String responseJson = requestProductsByCategory("Office");
assertThat(toDTOs(responseJson))
.extracting(ProductDTO::getId)
.containsOnly("1", "2");
assertThat(actualProduct.getPrice()).isEqualTo(100);
//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"));
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.
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 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));
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.
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.
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.
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.
* 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. ..
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
-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.
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());
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.
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).
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)
)
«@ 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() {}
}
}
@ Nested
dans JUnit 5 *@ DisplayName
ou backquote KotlinJava 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() {}
}
@ DisplayName
de JUnit5 *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`() {}
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");
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).
(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.
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.
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.
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.
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