• 10 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 09/02/2021

Améliorez la maintenabilité des tests fonctionnels avec les objets de page

Dans ce chapitre, nous allons consolider notre ascension au sommet de la pyramide des tests ! En effet, les tests fonctionnels sont les plus fragiles. Et le moindre changement dans une page web peut rendre votre test obsolète. Voyons comment nous pouvons améliorer la maintenabilité des tests fonctionnels de bout en bout, notamment grâce aux objets de page.

Évitez le piège des tests de bout en bout fragiles

Il existe un principe populaire dans le logiciel que vous devez toujours essayer de respecter. Cela s’appelle la règle DRY, pour « don’t repeat yourself » : « ne vous répétez pas ». Cela signifie que vous ne devez pas écrire le même code deux fois. Si vous le faites, c’est un bon moment pour nettoyer votre code avec le refactoring. Vous vous rappelez l'indicateur SonarCloud sur la duplication de code ?

Extrait du tableau de bord SonarCloud
Extrait du tableau de bord SonarCloud

C'est le même refactoring qui est utilisé dans le cycle rouge-vert-refactor ! Chaque répétition représente davantage de code que vous devez modifier si le comportement de votre application change. C’est aussi plus de code qui peut mal tourner.

Assez fréquemment lors de l’écriture des tests de bout en bout, vous avez besoin d'aller sur la même page à travers de multiples tests. Parfois, il vous faut cliquer sur un bouton ou remplir un champ. Quand il s’agit de déclarations d’une ligne, souvent dans un ordre différent, il est facile de ne pas remarquer que vous vous répétez.

Regardez ce bout de code :

	@Test
	public void aStudentUsesTheCalculatorToMultiplyTwoBySixteen() {

		// GIVEN
		webDriver.get(baseUrl);
		final WebElement leftField = webDriver.findElement(By.id("left"));
		final WebElement typeDropdown = webDriver.findElement(By.id("type"));
		final WebElement rightField = webDriver.findElement(By.id("right"));
		final WebElement submitButton = webDriver.findElement(By.id("submit"));

		// WHEN
		leftField.sendKeys("2");
		typeDropdown.sendKeys("x");
		rightField.sendKeys("16");
		submitButton.click();

		// THEN
		final WebDriverWait waiter = new WebDriverWait(webDriver, 5);
		final WebElement solutionElement = waiter.until(
				ExpectedConditions.presenceOfElementLocated(By.id("solution")));
		final String solution = solutionElement.getText();
		assertThat(solution).isEqualTo("32"); // 2 x 16
	}

Ce parcours utilisateur devait tester la multiplication. Imaginez que vous ayez d’autres tests pour valider les parcours pour différents types de calculs. Ce test pourrait inclure le code pour trouver des éléments de page et interagir avec eux pour effectuer la multiplication. Si vous écriviez plus de tests, vous pourriez rapidement vous retrouver avec une logique similaire dans beaucoup de tests !

Les tests fonctionnent, non ? Cela ne suffit-il pas ?

Regardons la partie de notre formulaire où nous avons sélectionné le calculationType (le type de calcul) :

   <form action="#" th:action="@{/calculator}" th:object="${calculation}" method="post">
...
                    <div class="col">
                        <select id="type" th:field="*{calculationType}" class="form-control form-control-lg">
                            <option value="ADDITION" selected="true"> + </option>
                            <option value="SUBTRACTION" selected="true"> - </option>
                            <option value="MULTIPLICATION"> x </option>
                            <option value="DIVISION"> / </option>
                        </select>
                    </div>
...
                </div>
                <div class="row">
                    <div class="col">
                        <button id="submit" type="submit" class="btn btn-primary">=</button>

                    </div>

...

Maintenant, imaginez ce qui se produirait si nous le transformions d’un menu déroulant en des cases à cocher ? Ou si un designer voulait restructurer et modifier le style de notre site, en créant de nouvelles pages HTML ? Nous verrions nos tests se casser et nous devrions trouver tous les tests qui pensent savoir comment fonctionne notre page, et les modifier !

Martin Fowler, l’un des fondateurs du mouvement Agile, a introduit le modèle PageObject. Il applique la programmation orientée objet à la façon dont nous concevons une page web. En remplacement du code ci-dessus, il s’agit de rassembler toutes ces méthodes et ces sélecteurs spécifiques à une page au même endroit. Un sélecteur, c’est ce  By.Id("left")  du code ci-dessus, qui demande à notre test de rechercher  <input id=”left">.

Les tests précédents étaient fragiles car de petits changements dans le HTML pouvaient les casser. Créons une classe PageObject pour notre calculateur ensemble pour éviter ces problèmes ! Partez de la branche p3ch5 du dépôt de code de ce cours :

git checkout -f p3ch5

Créez un nouveau paquetage com.openclassrooms.testoing.calcul.e2e.page et créez une nouvelle classe CalculatorPage. Déclarons les attributs de cette classe correspondant aux éléments que l'on peut trouver sur la page du Calculator :

public class CalculatorPage {

	@FindBy(id = "submit")
	private WebElement submitButton;

	@FindBy(id = "left")
	private WebElement leftArgument;

	@FindBy(id = "right")
	private WebElement rightArgument;

	@FindBy(id = "type")
	private WebElement calculationType;

	@FindBy(id = "solution")
	private WebElement solution;
	
	...
}

Chaque attribut est une instance de class WebElement, et l'annotation  @FindBy  permettra au WebDriver d'associer cet élément avec l'élément HTML ayant l'id indiqué. Ensuite, on code une méthode pour soumettre un calcul depuis cette page :

	private String calculate(String calculationTypeValue, String leftValue, String rightValue) {
		leftArgument.sendKeys(leftValue);
		calculationType.sendKeys(calculationTypeValue);
		rightArgument.sendKeys(rightValue);
		submitButton.click();

		final WebDriverWait waiter = new WebDriverWait(webDriver, 5);
		waiter.until(ExpectedConditions.visibilityOf(solution));

		return solution.getText();
	}

On y ajoute 4 méthodes pour faciliter les utilisateurs de cette classe pour les 4 opérations courantes. Elles appelleront toutes la méthode calculate. La classe finale CalculatorPage aura donc le code suivant :

public class CalculatorPage {

	public static final String ADDITION_SYMBOL = "+";
	public static final String SUBTRACTION_SYMBOL = "-";
	public static final String DIVISION_SYMBOL = "/";
	public static final String MULTIPLICATION_SYMBOL = "x";

	@FindBy(id = "submit")
	private WebElement submitButton;

	@FindBy(id = "left")
	private WebElement leftArgument;

	@FindBy(id = "right")
	private WebElement rightArgument;

	@FindBy(id = "type")
	private WebElement calculationType;

	@FindBy(id = "solution")
	private WebElement solution;

	private final WebDriver webDriver;

	public CalculatorPage(WebDriver webDriver) {
		this.webDriver = webDriver;
		PageFactory.initElements(webDriver, calculatorPage);
	}

	public String add(String leftValue, String rightValue) {
		return calculate(ADDITION_SYMBOL, leftValue, rightValue);
	}

	public String subtract(String leftValue, String rightValue) {
		return calculate(SUBTRACTION_SYMBOL, leftValue, rightValue);
	}

	public String divide(String leftValue, String rightValue) {
		return calculate(DIVISION_SYMBOL, leftValue, rightValue);
	}

	public String multiply(String leftValue, String rightValue) {
		return calculate(MULTIPLICATION_SYMBOL, leftValue, rightValue);
	}

	private String calculate(String calculationTypeValue, String leftValue, String rightValue) {
		leftArgument.sendKeys(leftValue);
		calculationType.sendKeys(calculationTypeValue);
		rightArgument.sendKeys(rightValue);
		submitButton.click();

		final WebDriverWait waiter = new WebDriverWait(webDriver, 5);
		waiter.until(ExpectedConditions.visibilityOf(solution));

		return solution.getText();
	}

}

Je vous invite à regarder la ligne 27 de plus près : c'est grâce à cet appel de PageFactory, une classe de Selenium, que les attributs annotés @FindBy sont initialisés. Cette ligne est donc très importante !

Voilà, nous avons créé notre premier objet de page ! Il ne reste plus qu'à l'utiliser ! Allons dans la classe de test StudentMultiplicationJourneyE2E et modifions le code du test  aStudentUsesTheCalculatorToMultiplyTwoBySixteen()  pour utiliser l'objet de page :

	@Test
	public void aStudentUsesTheCalculatorToMultiplyTwoBySixteen() {
		// GIVEN
		webDriver.get(baseUrl);
		final CalculatorPage calculatorPage = new CalculatorPage(webDriver);

		// WHEN
		final String solution = calculatorPage.multiply("2", "16");

		// THEN
		assertThat(solution).isEqualTo("32"); // 2 x 16

	}

C'est plus synthétique, non ? À l'étape GIVEN, on initie l'objet de page. Et les étapes WHEN et THEN sont clairement simplifiées, car une partie du code est traitée par CalculatorPage. Nous avons donc pu appeler calculatorPage.multiply(2, 16) et laisser la responsabilité de remplir les champs de formulaire appropriés au PageObject !

Vous pouvez alors coder d'autres tests simplement, par exemple un test d'addition :

	@Test
	public void aStudentUsesTheCalculatorToAddTwoToSixteen() throws InterruptedException {
		// GIVEN
		webDriver.get(baseUrl);
		final CalculatorPage calculatorPage = new CalculatorPage(webDriver);

		// WHEN
		final String solution = calculatorPage.add("2", "16");

		// THEN
		assertThat(solution).isEqualTo("18"); // 2 + 16
	}

Étant donné que vous disposez de toute votre connaissance du comportement d’une page web à un seul endroit, cela vous protège aussi de la dispersion de dépendances sur la structure de votre page web à travers de nombreux tests. Il n’y a qu’une dépendance sur la structure de toute page ; il s’agit de l’objet de page que vous avez modelé d’après elle.

Imaginez que dans calculator.html, l'identifiant du champ de type de calcul change (ligne 34 du fichier calculator.html). Le champ :

<select id="type" th:field="*{calculationType}" class="form-control form-control-lg">

devient :

<select id="typeOperation" th:field="*{calculationType}" class="form-control form-control-lg">

Vos deux tests vont échouer ! Mais pour les refaire passer en succès, il vous suffit de changer juste la ligne adaptée dans CalculatorPage :

	@FindBy(id = "type")
	private WebElement calculationType;

devient :

	@FindBy(id = "typeOperation")
	private WebElement calculationType;

Évitez la pyramide inversée

Vous êtes désormais armé de tous les outils nécessaires pour tester chaque niveau de la pyramide, mais méfiez-vous. Il est facile de se laisser séduire par le côté obscur du test. Plutôt que de choisir ce qui semble facile aujourd’hui, envisagez une approche qui vous permettra d’avoir une confiance continue en vos tests automatisés.

Il est facile de vous écarter de l’équilibre entre le feedback opportun et rapide et la confiance, en écrivant plus de tests vers le sommet de la pyramide, glissant ainsi vers un anti-pattern connu sous le nom de pyramide inversée.

La pyramide inversée ou cornet de glace des tests
La pyramide inversée ou cône de glace des tests

Lorsque nous parlons d’une nouvelle fonctionnalité dans une application, nous nous projetons directement dans l'interface utilisateur : « Afficher un message d’erreur en lecture seule à l’utilisateur ». Ceci pourrait vous conduire à choisir le mauvais test : la plupart des codeurs penseront immédiatement à écrire un test de bout en bout. Mais si vous avez un test d’intégration existant qui prouve que vous pouvez afficher des erreurs à l’utilisateur, un simple test d'intégration, voire unitaire, suffirait.

Plus vous avancez dans une application, plus il est facile de se tourner par défaut vers des tests situés plus haut dans la pyramide. Mais plus vous écrivez de tests de bout en bout, plus votre feedback est lent, et plus il faut de temps aux développeurs pour savoir s’ils sont sur la bonne voie. Vous pourriez même vous convaincre que ça va plus vite de faire des tests manuels ! Voici quelques signaux représentatifs que vous pouvez guetter, qui indiquent que votre pyramide commence à s’inverser :

  • vos tests se cassent souvent, et il est normal de devoir les réexécuter jusqu’à ce qu’ils réussissent ;

  • vos tests sont si peu fiables qu’il vous faut des tests manuels, juste pour être sûr ;

  • vos tests d’intégration et de bout en bout sont si lents que vous devez les exécuter séparément. Vous le faites peut-être pour une bonne raison, mais s’ils prennent des heures, vous devez vous demander ce que vous pouvez redescendre dans vos tests unitaires ;

  • votre équipe ne s’intéresse plus à ce que tous les tests fonctionnent ;

  • on n’écrit plus de nouveaux tests unitaires, ou ils ont seulement quelques tests de bons chemins ;

  • de nombreux bugs sont détectés par test manuel, ou par les utilisateurs peu après une sortie.

Restez vigilant ! 🙂

En résumé

  • Les modèles PageObject vous permettent d’englober les sélecteurs d’une page et de fournir des méthodes nommées de façon sémantique pour effectuer des actions sur cette page web.

  • L’utilisation du modèle PageObject vous protège de la dispersion des dépendances sur la structure de vos pages web.

  • Méfiez-vous de la pyramide inversée. Il est facile d’être entraîné à tout tester avec des tests fonctionnels de bout en bout. Ces tests ralentissent rapidement, et peuvent devenir peu fiables. 

Ce chapitre achève cette troisième et dernière partie sur les tests d'intégration et fonctionnels. Et si nous récapitulions tout ce que vous avez appris ? Rendez-vous au prochain et dernier chapitre !

Exemple de certificat de réussite
Exemple de certificat de réussite