User Tools

Site Tools


2019_2020:s3:methodo:td:env1:tests

This is an old revision of the document!


Tests en Java - Rappel ou Apprentissage

Voir pour plus de détail le livre p. 96 de ISBN 978-3-030-24094-3, Introduction to Software Design with Par Robillard, Martin P. Il devrait être disponible à la bibliothèque et vous trouverez une version électronique en ligne. (voir plus bas dans la catégorie référence).

Beaucoup des éléments qui suivent en sont une simple traduction.

Structuration d'une suite de tests

Une question courante lors de la construction d'une suite de tests unitaires est de savoir comment organiser nos tests de manière sensée. Il existe différentes approches, mais en Java un idiome commun est d'avoir une classe de test par classe de projet, où la classe de test rassemble tous les tests qui testent les méthodes ou autres scénarios d'utilisation qui impliquent la classe.

De plus, il est de pratique courante de localiser tout le code de test dans un dossier source différent avec une structure de paquet qui reflète la structure du paquet du code de production. La raison d'être de cette organisation est que dans Java les classes avec le même nom de paquet sont dans la même portée de paquet indépendamment de leur emplacement dans un système de fichiers. Cela signifie que les classes et les méthodes du paquet de test peuvent se référer à des classes non publiques (mais non-privés) des membres des classes du code de production, tout en étant toujours séparés du code de production.

Propriétés des tests

Écrire des tests unitaires pour des classes non triviales est souvent un processus créatif difficile, un peu comme écrire un code de production. Pour cette raison, il n'existe pas de formule ou de modèle standard pour écrire le code d'un test unitaire. En fait, parcourir les suites de tests de différents projets open-source montrera que les différentes communautés suivent des styles différents et utilisent des techniques de test différentes. Ceci étant dit, certains principes de base sont généralement acceptés, notamment que les tests unitaires doivent être rapides, indépendants, répétitifs, ciblés et lisibles[1].

  • Rapides. Les tests unitaires sont destinés à être exécutés souvent, et dans de nombreux cas dans le cadre d'un cycle de programmation-compilation-exécution. Pour cette raison, n'importe quelle suite de tests exécutée devrait être capable de se terminer en quelques secondes. Sinon, les développeurs seront tentés d'omettre de les exécuter, et les tests cesseront d'être utiles. Cela signifie que les tests unitaires doivent éviter les opérations de longue haleine telles que les E/S d'appareils intensifs et l'accès au réseau, et laisser le test de ces fonctions à d'autres tests que les tests unitaires. Il peut s'agir, par exemple, de tests d'acceptation ou d'intégration.
  • Indépendant. Chaque test unitaire devrait pouvoir être exécuté de façon isolée. Cela signifie, par exemple, qu'un test ne doit pas dépendre du fait qu'un autre test s'exécute avant de laisser un objet d'entrée dans un certain état. Premièrement, il est souvent souhaitable de n'effectuer qu'un seul test. Deuxièmement, tout comme le code, les suites de tests évoluent, avec l'ajout de nouveaux tests et (dans une moindre mesure) la suppression de certains tests. L'indépendance des tests facilite l'évolution de la suite de tests. Enfin, JUnit et d'autres tests de conception similaire n'offrent aucune garantie que les tests seront exécutés dans un environnement prévisible ou- der. En pratique, cela signifie que chaque test doit commencer par une nouvelle initialisation de l'état utilisé dans le cadre du test.
  • Répétable. L'exécution de tests unitaires devrait produire le même résultat dans des environnements différents (par exemple, lorsqu'ils sont exécutés sur des systèmes d'exploitation différents). Cela signifie que les oracles de test ne doivent pas dépendre de caractéristiques propres à l'environnement, telles que la taille de l'affichage, la vitesse du CPU ou les polices système.
  • Concentré. Les tests doivent exercer et vérifier une partie du comportement d'exécution du code qui est aussi étroite que raisonnablement possible. La raison d'être de ce principe est que le but des tests unitaires est d'aider les développeurs à identifier les pannes. Si un test unitaire comprend 500 lignes de code et teste toute une série d'interactions complexes entre objets, il ne sera pas facile de déterminer ce qui a mal tourné en cas d'échec. En revanche, un test qui vérifie une seule entrée sur un seul appel de méthode facilitera le repérage d'un problème. Certains ont même soutenu que les tests unitaires devraient comporter une seule affirmation[1]. Mon opinion 1) est que dans de nombreux cas, cela est trop strict et peut conduire à des inefficacités. Toutefois, les tests devraient idéalement se concentrer sur un seul aspect d'une unité à l'essai. Si l'unité testée est une méthode, nous pouvons l'appeler méthode focale pour le test.
  • Lisible. La structure et le style de codage du test devraient faciliter l'identification de toutes les composantes du test (unité sous test, données d'entrée, oracle), ainsi que la justification du test. Testons-nous l'initialisation d'un objet ? Un cas particulier ? Une combinaison particulière de valeurs ? Le choix d'un nom approprié pour le test peut souvent aider à clarifier sa raison d'être.

Retour sur l'indépendance des tests

En utilisant l'annotation @BeforeEach, nous indiquons à JUnit d'exécuter la méthode avant l'exécution de tout test. De même, il est également possible d'utiliser la notation @After Après une notation annotation pour marquer une méthode qui doit être exécutée après chaque test (par exemple pour libérer certaines ressources). Bien sûr, les objets immuables n'ont pas besoin d'être réinitialisés, ils peuvent donc être stockés comme champs statiques de la classe.

Test fixture

Une fixture est un morceau de code qui permet de fixer un environnement logiciel pour exécuter des tests logiciels. Cet environnement constant est toujours le même à chaque exécution des tests. Il permet de répéter les tests indéfiniment et d'avoir toujours les mêmes résultats. (Wikipedia)

package testRobillardBook;
 
import static org.junit.jupiter.api.Assertions.*;
 
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
 
public class TestFoundationPile {
                // Test Fisture
		private static final Card ACE_CLUBS = Card.get(Rank.ACE, Suit.CLUBS);
		private static final Card TWO_CLUBS = Card.get(Rank.TWO, Suit.CLUBS);
		private static final Card THREE_CLUBS = Card.get(Rank.THREE, Suit.CLUBS);
		private FoundationPile aPile;
		@BeforeEach
		public void setUp() { aPile = new FoundationPile(); }
 
		@Test
		public void testCanMoveTo_Empty() {
		    assertTrue(aPile.canMoveTo(ACE_CLUBS));
		    assertFalse(aPile.canMoveTo(THREE_CLUBS));
		  }
		@Test
		public void testCanMoveTo_NotEmptyAndSameSuit() {
		    aPile.push(ACE_CLUBS);
		    assertTrue(aPile.canMoveTo(TWO_CLUBS));
		    assertFalse(aPile.canMoveTo(THREE_CLUBS));
		} }
}

Couverture des tests

Il n'est pas toujours physiquement possible de tester de façon exhaustive l'espace d'entrée d'un projet. Par exemple, pour tester toutes les comparaisons de deux cartes d'un jeu de 54 cartes, il faudrait faire 1431 tests (ce qui reste faisable), mais pour tester les positions sur un jeu de dames, il y a 443 748 401 247 positions. Même avec un CPU de pointe, tester une méthode pour toutes les positions possibles cela prendrait beaucoup trop de temps et ceci est tout à fait incompatible avec l'exigence selon laquelle les tests unitaires doivent être exécutés “rapidement”.

De toute évidence, nous devons choisir les tests à réaliser parmi toutes les possibilités (par exemple, sélectionner les configurations du jeu de dame à tester). Le défi fondamental du problème de sélection des cas de test est de tester efficacement, c'est-à-dire de trouver un ensemble minimal de cas de test qui nous fournisse une quantité maximale de “test” pour notre code. Malheureusement, bien qu'il soit assez évident de savoir ce qu'est un nombre minimal de cas de test, il n'existe pas de définition naturelle ou même convenue de ce qu'est une “quantité de test”. Aujourd'hui il y a deux façons fondamentales d'aborder la sélection des cas de test :

  • Les tests fonctionnels (ou de boîte noire) tentent de couvrir autant que possible le comportement spécifié d'un programme, en se basant sur certaines spécifications externes de ce que le programme doit faire en particulier au travers de post-condition(Nous verrons également que dans le processus de production d'un programme, on prévoit des tests à réaliser dans les histoires utilisateurs). Les tests de boîte noire présentent de nombreux avantages, notamment le fait qu'il n'est pas nécessaire d'accéder au code, que les tests peuvent révéler des problèmes dans la spécification.
  • Les tests structurels (ou de boîte blanche) tentent de couvrir autant que possible le comportement implémenté d'un programme, sur la base d'une analyse du code source de l'unité à tester. Le principal avantage du test des boîtes blanches est qu'il peut révéler des problèmes causés par des détails d'implémentation de bas niveau qui sont invisibles au niveau de la spécification.

Références

1. Robert C. Martin. Clean Code: A Handbook of Agile Software Craftmanship. Prentice Hall, 2009.
2. Le livre de référence : https://coderprog.com/introduction-software-design-java/
3. Les codes sources associés au livre : https://github.com/prmr/DesignBook

1)
rappel extrait du livre de M. Robillard qui parle, je partage cependant cet avis
2019_2020/s3/methodo/td/env1/tests.1566736224.txt.gz · Last modified: 2019/08/25 14:30 by blay