Créez des API lisibles et flexibles
Il existe de nombreuses difficultés dans la création de tests d’automatisation e2e pour des systèmes complexes. L’un des nombreux est d’être dans un environnement « parfait ». Cet environnement doit être totalement sous votre contrôle et inclure un grand nombre, sinon la totalité, des caractéristiques d’un environnement de développement ou de production.
Ce n’est souvent pas le cas dans la pratique. L’environnement nécessite une supervision régulière, des réinitialisations de données et une configuration à la volée des exigences de chaque test.
Dans le cas des applications en ligne qui utilisent l’architecture REST et les requêtes API, nous pouvons contourner ce problème et configurer le système dans les conditions nécessaires avant chaque test ou suite de tests.
je vais utiliser Dramaturge pour cet exemple et le module de requête qu’il propose pour envoyer des requêtes API.
Disons que l’application que nous testons est une simple boutique de produits. Le JWT (JSON Web Token) est utilisée pour l’autorisation. Par conséquent, chaque appel suivant après la connexion doit inclure le jeton donné.
Un produit spécifique doit être en stock pour que certains tests fonctionnent. Par conséquent, l’utilisateur doit d’abord s’authentifier avant de recevoir le jeton et de l’utiliser pour constituer le stock d’un certain produit.
Envoi d’appels à l’intérieur du beforeAll
, qui s’exécute avant tous les tests du fichier ou de la suite de tests spécifié, est la ligne de conduite évidente. Voici comment procéder :
test.beforeAll(async (request) => {
let response = await request.post(` {
headers: this.standardHeaders,
data: {
username: "USERNAME",
password: "PASSWORD",
},
})let { accessToken, refreshToken } = JSON.parse(await response.text())
let setupCall = await request.post(` {
headers: {
authorization: `Bearer ${accessToken}`,
},
data: {
stock: 10,
},
})
if (setupCall.status() == 200) {
console.log("Product is now setup correctly")
}
})
test("Test that requires product with id 00001 in stock", async (request) => {
// TEST ITSELF RUNS HERE
})
Cette stratégie présente plusieurs inconvénients. Le plus évident est la lisibilité du code car le test est bourré de code inutile qui n’a pas grand-chose à voir avec son fonctionnement principal. La seconde est la redondance du code car la fonctionnalité de configuration du produit nécessitera de répéter le code pour chaque test. Pour dix tests, l’utilisateur doit se connecter dix fois, ce qui fait perdre du temps et des ressources et cause un problème de performances.
Comme beaucoup d’autres problèmes architecturaux, ce problème peut être résolu en utilisant des concepts de POO. En développant une classe pour construire les données d’un test, une manière d’accéder à l’API est fournie qui est standardisée, reproductible et évolutive.
La classe ressemblerait à ceci :
class SetupCalls {
constructor(request) {
this.baseUrl = "https://someapp.com"
this.accessToken = null
this.refreshToken = null
}async getToken(username, password) {
this.request = await request.newContext()
let response = await this.request.post(`${this.baseUrl}/api/login`, {
headers: this.standardHeaders,
data: {
username: username,
password: password,
},
})
let { accessToken, refreshToken } = JSON.parse(await response.text())
this.accessToken = accessToken
this.refreshToken = refreshToken
}
async setStock(productId) {
let setupCall = await this.request.post(`${this.baseUrl}/product/${productId}`, {
headers: {
authorization: `Bearer ${this.accessToken}`,
},
data: {
stock: 10,
},
})
if (setupCall.status() == 200) {
console.log("Product is now setup correctly")
}
}
}I
Maintenant que les fonctions ont été efficacement encapsulées, il est assez simple de comprendre ce que fait chaque fonction. Plusieurs appels peuvent être effectués à l’aide du même jeton car il est partagé au sein de la classe elle-même, ce qui permet de gagner du temps en évitant de devoir se connecter à plusieurs reprises. Voyons à quoi ressemble actuellement le test :
test.beforeAll(async () => {
let setupCalls = new SetupCalls()
await setupCalls.getToken('John', 'john123')
await setupCalls.setStock("00001")
await setupCalls.setStock("00002")
await setupCalls.setStock("00003")
})test("Test that requires product with id 00001 in stock", async (request) => {
// TEST ITSELF BEGINS
})
Bien que la mise en œuvre semble être excellente, il y a encore quelques problèmes. Un utilisateur doit comprendre le fonctionnement spécifique de la classe setup avant de l’utiliser. Si la getToken
Si la méthode n’est pas appelée avant toutes les autres, les tests échoueront car le jeton ne sera pas rempli.
L’appel au jeton getToken
devrait idéalement être fait dans le constructeur, cependant, le constructeur ne peut pas être une fonction asynchrone. L’utilisation de Promises
semble donc être la réponse.
constructor() {
this.baseUrl = "https://someapp.com"
let {accessToken, refreshToken} = Promise.resolve(getToken()).then(res => {
// do something with the token
});
}j
Cependant, cela altère le comportement normal du constructeur et nécessite une préservation soigneuse de ce contexte. Cette approche compromet l’objectif d’utilisation d’async-wait, qui est de garder le code lisible et simple.
Au lieu d’un constructeur traditionnel, nous pouvons initialiser un objet avec un fonction/méthode statique standard et utilisez des méthodes asynchrones à l’intérieur sans perdre aucun des avantages que cette dernière implémentation nous a fournis.
Les méthodes statiques ne sont pas appelées sur les instances de la classe mais plutôt sur la classe elle-même. Rendre le constructeur privé garantit que l’utilisateur initialise un objet en utilisant init plutôt que le constructeur par défaut. La classe ne reçoit que les informations nécessaires, telles que le jeton d’accès, par la méthode init de la classe, qui crée une nouvelle instance de classe.
Jetons un œil au code :
class SetupCalls {
/**
* @private
*/
constructor(config) {
this.baseUrl = "https://someapp.com"
this.accessToken = config.accessToken
this.refreshToken = config.refreshToken
}static async init(username, password) {
let config = await this.getToken(username, password)
return new SetupCalls(config)
}
async getToken(username, password) {
this.request = await request.newContext()
let response = await this.request.post(` {
data: {
username: username,
password: password,
},
})
return JSON.parse(await response.text())
}
async setStock(productId) {....}
}
Le test est désormais beaucoup plus simplifié et utilise async/wait dans chaque situation :
test.beforeAll(async () => {
let setupCalls = await SetupCalls.init("John", "John1213!")
await setupCalls.setStock("00001")
})test("Test that requires product with id 00001 in stock", async (request) => {
// TEST ITSELF BEGINS
})
Il est souvent possible de configurer les données avant même que les tests ne soient exécutés. Le globalSetup
drapeau dans le playwright.config.js
Le fichier de configuration nous permet de spécifier un emplacement pour le fichier qui gère la configuration globale :
const config = {
globalSetup: require.resolve("./globalSetup"),
testDir: "./tests",
timeout: 80 * 1000,
expect: {
timeout: 5000,
},
...
}
Maintenant que nous avons construit la classe, le fichier peut l’appeler et configurer l’ensemble de données dans une seule fonction. Nous pouvons même configurer tous les appels pour qu’ils s’exécutent en parallèle pour accélérer nos tests, car les appels pour la mise en place de produits ne sont pas liés. C’est ce que le globalConfig.js
le fichier ressemblerait à :
module.exports = async (config) => {
let setupCalls = await SetupCalls.init(config.username, config.password)
let dataset = ["00001", "00002", "00003", "00004"]const responses = await Promise.all(
dataset.map(async (id) => {
const res = await setupCalls.setStock(id)
})
)
console.log(responses)
}
Si vous effectuez des tests d’API ou utilisez simplement l’API pour la préparation des données ou une autre automatisation, il est toujours judicieux d’organiser les appels de manière à permettre la flexibilité tout en offrant lisibilité et structure.
En raison de sa nature asynchrone, JavaScript peut parfois prêter à confusion. Ainsi, garder les choses simples tout en maintenant les performances peut faire la différence entre un code passable et un excellent code.