Si vous ne faites pas de tests, désolé pour vous, mais cet article ne va pas beaucoup vous intéresser… Ici on va parler du pattern Page Object, Persona et structuration des tests. Parce qu'on parle trop souvent de "pattern de code" pour le code de production mais pas assez de pattern de test alors que c'est au moins aussi important !
Note 1 : je vais faire tous mes exemples avec Playwright, mais sur le fond, on s'en fiche de Playwright, le pattern Page Object et les bonnes pratiques élémentaires sont beaucoup plus anciens que Playwright (je les ai appris en faisant du Selenium !), et s'adaptent à tous les frameworks de tests IHM qui tiennent la route ! Après si vous cherchez un bon framework de test : prenez Playwright, ça fonctionnera et vous ne vous prendrez pas trop la tête !
Note 2 : Là je suis sur une orientation tests d'IHM, mais une bonne partie de ce que je vais raconter s'applique parfaitement à un test d'API en Java avec SpringTest ou même à votre batch Python (exemples au hasard, mais vous avez l'idée, c'est un exemple, rien de plus).
Des E2E à l'arrache…
Ne le prenez pas mal mais c'est la vérité, en général je vois beaucoup de test E2E (ou test d'IHM, ou bout en bout, ou UI, … appelé ça comme vous voulez) qui ne sont pas fait proprement, pas maintenables, pas lisibles !
Exemple du genre de test que je vois tout le temps :
test("Détail commande", async ({ page }) => {
await page.goto("http://localhost:4321/");
await expect(page.getByText("Accueil")).toBeVisible();
await expect(page.getByTestId("item-3546")).toBeVisible();
await expect(page.getByTestId("item-3500")).toBeVisible();
await page.getByTestId("item-3546_addToCart").click();
await page.getByTestId("item-3500_addToCart").click();
await expect(page.locator(".bi-basket")).toBeVisible();
await page.locator(".bi-basket").click();
await expect(page.getByText("Panier")).toBeVisible();
// changement quantité
await page.getByTestId("item-3546_plus").click();
await expect(page.getByTestId("item-3546_qte")).toHaveValue("2");
await page.getByTestId("item-3546_minus").click();
await expect(page.getByTestId("item-3546_qte")).toHaveValue("1");
await page.getByTestId("item-3546_minus").click();
await expect(page.getByTestId("item-3546")).not.toBeVisible();
// valide panier
const total = await page.getByTestId("total").textContent();
await page.getByTestId("btn-validate-no-account").click();
await page.getByTestId("firstname").fill("john");
await page.getByTestId("lastname").fill("doe");
await page.getByTestId("address").fill("10 rue du Paradis");
await page.getByTestId("zipcode").fill("44000");
await page.getByTestId("city").fill("Nantes");
await expect(page.getByTestId("error")).toHaveText("Invalid Email");
await page.getByTestId("email").fill("john@doe.com");
await page.locator(".btn-primary").click();
// payer
await expect(page.getByText("Paiement")).toBeVisible();
await expect(page.getByTestId("btn-pay")).toBeDisabled();
await page.getByTestId("cb-field").fill("0000-0000-0000-0000");
await page.locator(".btn-primary").click();
await expect(page.getByText("error")).toHaveText("Vous devez saisir la date d'expiration.");
await page.getByTestId("mm-field").fill("05");
await page.getByTestId("yy-field").fill("25");
await page.getByTestId("cvv-field").fill("909");
await page.locator(".btn-primary").click();
// détail
await expect(page.getByText("Commande validée")).toBeVisible();
await expect(page.getByText("Montant")).toBeVisible();
await expect(page.getByTestId("total")).toHaveText(total);
});
Maintenant : que fait ce test ? que test ce test ?
Vous vous dites "non mais de but en blanc c'est compliqué, mais si connaît le projet ça se comprend et c'est logique !", mais moi cette réponse ne me convient pas. Pareil si vous vous dites "Oui bah on a fait comme les autres test...", ça non plus ça ne me convient pas.
Pour moi les tests c'est une documentation "vivante" : elle est exécutable, elle se vérifie face à la réalité (l'application) donc je sais qu'elle est censée être à jour. Quand je débarque sur une application, je jette un coup d'œil aux tests pour savoir comment elle fonctionne. En tout cas j'essaie. Et rares sont les cas où les tests sont assez bien écrits / structurés pour me donner le niveau d'information que je voudrais avoir… Et pourtant c'est peu d'effort de passer d'un empilement de code comme au-dessus (parce que pour moi c'est pas plus qu'un amas de code) à des tests structuré et orienté métier !
On veut tester quoi ?
La première question c'est toujours "on veut tester quoi ?". Dans l'extrait je peux déjà vous dire qu'on teste beaucoup trop de choses d'un coup et pas de manière claire !
Commençons par la base : le nom du test. Le nom du test c'est là qu'on est censé indiquer la règle métier qu'on veut valider. Ici c'est "Détail commande". On a un contexte, c'est cool, mais on va tester quoi ? Que le détail de commande s'affiche ? Ne s'affiche pas ? Après une commande seulement ? Que le détail de la commande est accessible ? À n'importe qui ? Via URL directe ? Via navigation ? Qu'on retrouve les données attendues ? Rien à voir parce qu'on a copier-coller d'un autre test et on a pas fait attention qu'on avait pas changé le titre du test ?
Ensuite un gros indice que ce test n'est pas bon : il fait 49 lignes avec des groupes délimités par des lignes vides dont certains avec des commentaires... Vous le sentez qu'on teste tout et rien à la fois avec ce genre de test ? En effet, si vous avez un test comme ça, vous allez réduire pas mal le nombre d'assertion à chaque étape vu que vous n'avez pas envie de le répéter à chaque test et avoir des tests à rallonges.
Quand bien même vous faite partie de la catégorie de gens qui pensent que "les commentaires ça ne sert à rien c'est jamais à jour, y'a que le code qui a raison", vous voyez bien que c'est presque impossible de savoir ce que cherche à tester ce test en un coup d'œil, non ?
Partons de la spécification fonctionnelle :
En tant qu'utilisateur, je veux pouvoir voir le détail de ma commande après le paiement.
En tant qu'utilisateur je veux pouvoir accéder au détail d'une commande via un lien si c'est ma commande.
En tant qu'utilisateur, je veux retrouver sur le détail de ma commande : la date, mon nom, mon adresse, la liste des articles (libellé, quantité, montant), le montant total, le status de la commande et le lien de suivi.
En tant qu'utilisateur, je dois pouvoir faire une réclamation une fois la commande expédiée
Définissons clairement ce qu'on veut tester à partir de ça :
- Le détail d'une commande s'affiche après le paiement ;
- Le détail d'une commande est accessible via une URL en direct ;
- Je ne peux pas accéder à la commande d'un autre utilisateur ;
- Le détail d'une commande doit afficher les infos de la commande ;
- Une commande en status "Validé" doit afficher les infos de la commande sauf le lien de suivi ;
- Une commande en status "Expédié" doit afficher les infos de la commande avec un lien de suivi ;
- Une commande en status "Expédié" doit afficher un lien de réclamation ;
Je ne vous ai pas donné le code de chaque test (pour l'instant), mais vous voyez bien qu'on a une idée ce qui se cache derrière comme code ? Et que si on suit le même schéma que le test que je vous ai montré plus haut, on aura facilement 150-200 lignes de code à maintenir et des tests instables ? Non ?
Plein de tests = exécution longue = mauvais ?
J'entends parfois "on a regroupé des tests pour aller plus vite !". C'est vrai mais est-ce que c'est un vrai gain de temps ?
Avoir un gros test qui fait tout est en effet souvent plus rapide que plusieurs petits tests, car moins de temps à attendre la CI et moins de code donc moins de maintenance. Sauf que… dans les faits c'est faux.
Il faut prendre en compte 4 points :
- Vous n'attendez réellement la CI que rarement ;
- Un gros test implique aussi plus d'instabilité (donc au choix du rejeu automatique ou manuel, donc la grosse perte de temps à rejouer) ;
- Des gros tests, ça implique plus d'effort pour maintenir chaque test ;
- Un gros test implique qu'on aura plus de mal à comprendre ce qui ne va pas en cas d'erreur ;
Prenons un exemple : si 100% de mes tests font un parcours complet (parcours d'article, panier, paiement, détails en confirmation), et que j'ai disons 100% des tests en échec, quel est le problème ? Et si j'ai 50% des tests en échec ? On en sait rien sans aller mettre le nez dans le code du test et qu'on inspecte la ligne où il y a une erreur, peut-être sur plusieurs tests pour faire une corrélation et déduire le problème.
Autre cas : j'ai environ 25% des tests qui ne font que du parcours d'article, 25% qui manipule le panier (avec ou sans parcours d'article), 25% qui vont au paiement et 25% qui manipule le détail de l'offre, que les tests sont regroupés par fonctionnalités (parcours, panier, etc.) ; j'ai 50% des tests qui plantent une grosse partie des tests du panier, presque tous les tests du paiement, une partie des tests du détail ; où est l'erreur ? Instinctivement, je me dis que l'erreur est surement au niveau de la validation du panier ou de la validation du paiement.
Vous voyez la différence ? Là on ne parle pas de lire du code, mais d'analyser des résultats de tests.
Mais des petits tests, ça veut dire beaucoup se répéter !
À priori on entend jamais les développeurs se plaindre de voir du code se répéter à droite à gauche de la base de code. Pourquoi ? Quand on dev on factorise le code. Ça tombe bien : les tests automatisés c'est du code, donc on peut faire la même chose ! 🤯 (Si vous êtes étonné ici, je suis assez surpris j'avoue 😅)
En fait il y a un pattern bien connu dans le monde du test qui permet de résoudre en grande partie ça : le Page Object Pattern (ou juste Page Object). En bonus de la factorisation du code, ça va permettre de très simplement rendre nos tests beaucoup plus lisibles et orienté métier !
L'idée est simple, on va partir de l'idée qu'une brique de l'application au sens fonctionnel du terme ça va être un Page Object. Et on va découper tout comme ça en regroupant dans les Pages Objects nos locators (en Playwright, l'appel à page.getByXXX()
qui permet la sélection d'un élément de la page), nos comportements génériques, etc. Et bim ça fait des chocapics tests propres !
Mais un Page Object c'est quoi concrètement ? C'est ultra compliqué, vous allez voir : c'est une classe avec des méthodes. Et c'est tout.
Un exemple ? Prenons un cas super simple !
test("Une commande en status \"Expédié\" doit afficher un lien de réclamation", async ({ page }) => {
await page.goto("http://localhost:4321/order/B3F7DD09");
await expect(page.getByRole("link", { name: "Réclamation" })).toBeVisible();
await page.getByRole("link", { name: "Réclamation" }).click();
await expect(page.getByRole("heading", { name: "Réclamation" })).toBeVisible();
});
J'ai pris un des exemples que je citais plus haut dans les cas fonctionnels : on va sur la page d'une commande via son identifiant, ensuite on vérifie qu'on voit bien le lien Réclamation, on clique dessus, puis on s'assure qu'on arrive bien sur la page de Réclamation.
Donc déjà le test court fait qu'il se lit bien. Bon point déjà. Par contre, on se répète sur le locator du lien réclamation, on a pas mal d'élément technique inutile, on a le lien en dur de la page donc si un jour elle change ça va être fastidieux de repasser partout (et je parle même pas de gérer des environnements…), pareil si le texte du lien "Réclamation" change (pour devenir "Réclamations" par exemple), c'est pas mal de travail de tout changer.
Moi je veux avoir ça comme test :
// <root>/tests/order-details.spec.ts
import { test } from '../fixtures';
test("Une commande en status \"Expédié\" doit afficher un lien de réclamation", async ({ orderDetailPage, claimPage }) => {
await orderDetailPage.go({ orderId: 'B3F7DD09' });
await expect(orderDetailPage.claimLink()).toBeVisible();
await orderDetailPage.claimLink().click();
await expect(claimPage.title()).toBeVisible();
});
À part les await
on est presque sur des phrases là, non ?
Et la ✨ "magie" ✨ derrière ?
// <root>/page-objects/order-detail.page.ts
import { Page } from '@playwright/test';
export class OrderDetailPage {
constructor(private page: Page) {}
go(params: { id: string }) {
return this.page.goto(`http://localhost:4321/order/${id}`);
}
claimLink() {
return page.getByRole("link", { name: "Réclamation" });
}
}
// <root>/page-objects/claim.page.ts
import { Page } from '@playwright/test';
export class ClaimPage {
constructor(private page: Page) {}
title() {
return page.getByRole("heading", { name: "Réclamation" });
}
}
// <root>/fixtures.ts
import { Page, test as base } from "@playwright/test";
import { ClaimPage } from "./page-objects/claim.page";
import { OrderDetailPage } from "./page-objects/order-detail.page";
export * from "@playwright/test";
export interface AppFixtures {
claimPage: ClaimPage;
orderDetailPage: OrderDetailPage;
}
export const test = base.extend<AppFixtures>({
claimPage: declarePO(HomePage),
orderDetailPage: declarePO(DiscoveriesPage),
});
function declarePO(PageObject: { new (page: Page): void }) {
return async ({ page }, use) => {
await use(new PageObject(page));
};
}
Alors est-ce que vous êtes impressionné par cette ✨ "magie" ✨ bien obscure ? Non je pense pas. Est-ce que ça demande un peu de rigueur au moins au début ? Oui complètement ! Mais franchement l'effort est minuscule pour un gain énorme au long terme !
Et pour le reste ?
Pour la suite c'est pas bien compliqué : on déroule tous les tests en ajoutant les locators dont on a besoin dans les pages objects. Vous pouvez même très bien vous faire des méthodes "raccourcis" pour gagner du temps sur certains parcours (par exemple une méthode dans votre Page Object pour remplir directement un formulaire en donnant un json avec tous les champs). Et n'hésitez pas à simplifier le parcours comme vous avez d'autres tests pour valider les différentes étapes.
Par exemple :
test("Le détail d'une commande s'affiche après le paiement", async ({ basketPage, headerPage, homePage, recommandedArticlesPage, orderDetailPage, paymentPage }) => {
// Pay an order with 2 items
await homePage.go();
await recommandedArticlesPage.addToCart("item-3546");
await recommandedArticlesPage.addToCart("item-3500");
await headerPage.basketIcon().click();
await basketPage.fillCustomerInfos({
firstname: "john",
lastname: "doe",
address: "10 rue du Paradis",
zipcode: "44000",
city: "Nantes",
email: "john@doe.com"
});
await basketPage.validate();
await paymentPage.fillCardInfos({
card: "0000-0000-0000-0000",
expiration: {month: "05", year: "25"},
cvv: "909"
});
await paymentPage.payButton().click();
// Order detail should be visible
await expect(orderDetailPage.validatedMessage()).toBeVisible();
});
Est-ce que ce test est lisible ? À mon avis oui. En tout cas beaucoup plus que le test du début ! Et pourtant on fait le même parcours (juste quelques validations explicites de moins) !
Pour être honnête y'a un élément qui m'embête encore très fort : les jeux de données. Comment on maintient dans le temps tout ça ? On va répéter les infos utilisateur partout ? Et les identifiants des articles ? On y va yolo partout aussi ?
Le Persona Pattern
Je ne sais pas vraiment si c'est un pattern "reconnu" mais en tout cas c'est un pattern que j'aime bien utiliser pour isoler les jeux de données, et avoir une sorte de référentiel des jeux de données facile à trouver.
C'est en plus hyper pratique pour s'orienter dans les tests ! Par exemple vous pouvez créer un persona utilisateur Anaïs qui a déjà un compte utilisateur, avec des commandes, une carte bancaire pré-enregistrée. Vous avez un persona utilisateur Ben qui n'a jamais créé de compte. Vous avez un persona utilisateur Catherine qui a une carte bancaire expirée. Etc. Vous pouvez les lister, les documenter, et les réutiliser dans tous vos tests !
Astuce : Vous pouvez profiter du listing de persona pour diversifier les origines de prénoms / noms pour être représentatifs et tester certains cas parfois embêtants ! Quelques exemples : des noms de famille à une seule lettre (pas besoin de chercher très loin pour en trouver, on a par exemple eu Cédric O dans le gouvernement), à l'inverse des noms typés malgaches qui ont tendances à être très long (exemples : Rakotomalala, Rafanomezantsoa ou Randrianarimanana), des noms avec des particules et/ou des espaces et/ou des tirets, des prénoms composés (Jean-François), etc.
Et de la même façon que vous pouvez avoir un listing de Persona utilisateur, n'hésitez pas à avoir des "Persona item" par exemple pour avoir différent type de produits (ou quelque soit votre objet métier de référence) pour manipuler des éléments un peu différent en fonction des tests mais garder une certaine cohérence en même temps !
Et du coup il faut une structure compliquée pour gérer tout ça ? Que nenni ! On prend quelque chose de simple : une classe ou un objet simple. Personnellement je préfère un simple objet en TypeScript, mais ne vous privez pas de structurer plus si vous estimer en avoir besoin !
// <root>/persona.ts
export const Persona = {
user: {
John: {
firstname: "john",
lastname: "doe",
address: "10 rue du Paradis",
zipcode: "44000",
city: "Nantes",
email: "john@doe.com",
cb: {
card: "0000-0000-0000-0000",
expiration: {month: "05", year: "25"},
cvv: "909"
}
}
},
item: {
LightSaber: {
id: "3546"
},
OneRing: {
id: "3500"
}
}
} as const;
Ensuite dans les tests :
test("Le détail d'une commande s'affiche après le paiement", async ({ basketPage, headerPage, homePage, recommandedArticlesPage, orderDetailPage, paymentPage }) => {
// Pay an order with 2 items
await homePage.go();
await recommandedArticlesPage.addToCart(Persona.item.LightSaber);
await recommandedArticlesPage.addToCart(Persona.item.OneRing);
await headerPage.basketIcon().click();
await basketPage.fillCustomerInfos(Persona.user.John);
await basketPage.validate();
await paymentPage.fillCardInfos(Persona.user.John.cb);
await paymentPage.payButton().click();
// Order detail should be visible
await expect(orderDetailPage.validatedMessage()).toBeVisible();
});
Ce test est quand même beaucoup plus lisible non ?
ApiPage
pour gagner du temps !
Le test précédent est plutôt complet et c'est vraiment utile ce genre de test !
Maintenant, c'est aussi un peu "lent" au sens ou faire tout le parcours tout le temps c'est pas toujours pratique… Et parfois on a besoin d'un jeu de donnée précis et qu'on ne peut pas forcément garder des jeux de données vivants…
L'astuce c'est de créer un Page Object qui ne correspond à rien de visible à l'écran, mais qui va faire des appels à l'API de votre application directement, pour récupérer un token d'accès, créer des données (ici des commandes par exemple), simuler des actions utilisateurs / administrateurs (comme valider des commandes, ou les marquer comme expédiée / annulée).
L'idée c'est vraiment de se faire des raccourcis pour simplifier très fortement vos tests !
La principale différence entre ApiPage
et les autres Page Object, c'est qu'il ne va pas utiliser la builtin fixtures page
mais request
. Je passe l'implémentation qui est très simple pour vous laisser regarder comme l'utiliser dans la partie suivante.
Tous les tests de cette feature
Pour rappel, plus haut, on avait défini qu'on voulait ces cas de test :
- Le détail d'une commande s'affiche après le paiement ;
- Le détail d'une commande est accessible via une URL en direct ;
- Je ne peux pas accéder à la commande d'un autre utilisateur ;
- Le détail d'une commande doit afficher les infos de la commande ;
- Une commande en status "Validé" doit afficher les infos de la commande sauf le lien de suivi ;
- Une commande en status "Expédié" doit afficher les infos de la commande avec un lien de suivi ;
- Une commande en status "Expédié" doit afficher un lien de réclamation ;
Donc allons-y !
import { test } from '../fixtures';
test.describe('parcours complet', () => {
test("Le détail d'une commande s'affiche après le paiement", async ({
basketPage, headerPage, homePage, recommandedArticlesPage, orderDetailPage, paymentPage
}) => {
// Pay an order with 2 items
await homePage.go();
await recommandedArticlesPage.addToCart(Persona.item.LightSaber);
await recommandedArticlesPage.addToCart(Persona.item.OneRing);
await headerPage.basketIcon().click();
await basketPage.fillCustomerInfos(Persona.user.John);
await basketPage.validate();
await paymentPage.fillCardInfos(Persona.user.John.cb);
await paymentPage.payButton().click();
// Order detail should be visible
await expect(orderDetailPage.validatedMessage()).toBeVisible();
});
});
test.describe('Accès direct', () => {
test("Le détail d'une commande est accessible via une URL en direct", async ({ apiPage, orderDetailPage }) => {
const orderId = await apiPage.prepareBasicOrder(Persona.user.John);
const accessToken = await apiPage.loginAs(Persona.user.John);
await orderDetailPage.go({ orderId, accessToken });
await expect(orderDetailPage.orderId()).toContainText(orderId);
});
test("Je ne peux pas accéder à la commande d'un autre utilisateur", async ({ apiPage, orderDetailPage }) => {
const orderId = await apiPage.prepareBasicOrder(Persona.user.John);
const accessToken = await apiPage.loginAs(Persona.user.Jasmine);
await orderDetailPage.go({ orderId, accessToken });
await expect(orderDetailPage.unexistingOrderMessage()).toBeVisible();
});
test("Le détail d'une commande doit afficher les infos de la commande", async ({ apiPage, orderDetailPage }) => {
const orderId = await apiPage.prepareBasicOrder(Persona.user.John);
const order = await apiPage.getOrderInfo(orderId);
const accessToken = await apiPage.loginAs(Persona.user.John);
await orderDetailPage.go({ orderId, accessToken });
await expect(orderDetailPage.orderId()).toContainText(orderId);
await expect(orderDetailPage.orderDate()).toHaveText(/\d{2}\/\d{2}\/\d{4}/);
await expect(orderDetailPage.firstname()).toHaveText(Persona.user.John.firstname);
await expect(orderDetailPage.lastname()).toHaveText(Persona.user.John.lastname);
await expect(orderDetailPage.address()).toHaveText(Persona.user.John.address);
await expect(orderDetailPage.totalAmount()).toHaveText(order.totalAmount);
await expect(orderDetailPage.orderStatus()).toHaveText(order.orderStatus);
});
test.describe("Infos conditionnelles en fonction du status de la commande", () => {
test(`Une commande en status "Validé" doit afficher les infos de la commande sauf le lien de suivi`, () => {
const orderId = await apiPage.prepareBasicOrder(Persona.user.John);
const accessToken = await apiPage.loginAs(Persona.user.John);
await orderDetailPage.go({ orderId, accessToken });
await expect(orderDetailPage.tackingLink()).not.toBeVisible();
});
test(`Une commande en status "Expédié" doit afficher les infos de la commande avec un lien de suivi`, () => {
const orderId = await apiPage.prepareBasicOrder(Persona.user.John);
await apiPage.simulateStatus(orderId, 'SENT');
const accessToken = await apiPage.loginAs(Persona.user.John);
await orderDetailPage.go({ orderId, accessToken });
await expect(orderDetailPage.tackingLink()).toBeVisible();
});
test(`Une commande en status "Expédié" doit afficher un lien de réclamation`, () => {
const orderId = await apiPage.prepareBasicOrder(Persona.user.John);
await apiPage.simulateStatus(orderId, 'SENT');
const accessToken = await apiPage.loginAs(Persona.user.John);
await orderDetailPage.go({ orderId, accessToken });
await expect(orderDetailPage.claimLink()).toBeVisible();
await orderDetailPage.claimLink().click();
await expect(claimPage.title()).toBeVisible();
});
});
});
Je pense qu'avec un peu d'aide au début des gens du métier (PO / PM par exemple) pourrait lire ce code on pourrait en discuter ensemble.
Conclusion
On est parti d'un test assez classique qui faisait une cinquantaine de ligne, qu'on ne comprenait pas bien (pas du tout ?), difficile à maintenir dans le temps, difficile à manipuler, qui portait peu d'information, pour arriver à sept tests de quelques lignes chacun, en tout 86 lignes de code (import inclut !), avec un titre qui nous dit directement ce qu'on cherche à valider, facile à maintenir, avec juste un peu de structuration et de code extrait. Avec un tout petit peu de factorisation, on est passé d'un test qui ne tient pas dans l'écran à un ensemble de tests facile à lire.
À noter que tout ça peut totalement cohabiter avec du code qui n'utilise pas les patterns Page Object et Persona. Ce n'est que de la structuration de code, il n'y a pas de magie, vous pouvez donc migrer au fil de l'eau vers ça pour améliorer petit à petit votre base de test.
Ce que je peux dire c'est que je vous parle de ça avec mon expérience autour du test, c'est mon regard, mais j'ai l'impression que tout ça paie vraiment sur le long terme !
Est-ce que tout ça vous a semblé compliqué ? Une évidence ? Une pure découverte ? Quelque chose d'inatteignable ? Trop d'effort ? Dites-moi en commentaire sur les réseaux sociaux ou par message privé, je suis ouvert à la discussion et je veux bien prendre le temps de comprendre si vous pensez qu'il y a des freins réels à utiliser les techniques que je vous ai montrés.
Sources:
- Page Object par Martin Fowler
- Playwright
Crédit photo : Générée via Mistral AI avec le prompt suivant
Créez une illustration détaillée et métaphorique représentant un grand navire en bois naviguant sur une mer légèrement agitée sous un ciel étoilé. Le navire, symbolisant un projet logiciel, doit être bien construit et robuste, avec des voiles gonflées par le vent, indiquant un voyage en cours.
À la proue du navire, un capitaine tient fermement une carte détaillée et une boussole, symbolisant les tests logiciels qui guident le projet. La carte doit être visible avec des détails complexes, et la boussole doit briller légèrement, indiquant la direction.
Sur le pont, plusieurs membres d'équipage, représentant l'équipe de développement, travaillent ensemble de manière organisée. Certains manipulent des cordes et des voiles, tandis que d'autres observent l'horizon avec des télescopes, symbolisant la vigilance et la collaboration.
Autour du navire, plusieurs phares sont visibles à l'horizon, émettant des faisceaux de lumière qui percent la brume légère. Ces phares représentent les Page Objects et les Personas, offrant des points de repère stables et fiables pour guider le navire.
Le ciel nocturne est rempli d'étoiles brillantes, formant des constellations reconnaissables qui symbolisent les objectifs à long terme et la vision du projet. Les étoiles doivent être lumineuses et bien définies, offrant une orientation constante.
La mer doit montrer quelques vagues, symbolisant les défis et les incertitudes du développement logiciel, mais globalement navigable, indiquant que le navire est bien préparé pour affronter ces défis.
En arrière-plan, une côte lointaine avec des falaises et des arbres peut être visible, représentant la destination finale du projet. La côte doit être partiellement éclairée par la lumière des phares, indiquant un avenir prometteur et sécurisé.
L'image doit être réaliste mais avec une touche de fantaisie pour renforcer la métaphore, utilisant des couleurs chaudes pour les phares et les étoiles, contrastant avec les tons bleus et verts de la mer et du ciel.