Meilleures techniques de test en JavaScript et Node.js


Il s'agit d'un guide complet pour assurer la fiabilité de JavaScript et de Node.js. Voici des dizaines de meilleurs articles, livres et outils.

Tout d'abord, traitez les méthodes d'essai généralement acceptées qui sous-tendent toute application. Et puis, vous pouvez vous plonger dans le domaine qui vous intéresse: frontend et interfaces, backend, CI ou tout ce qui précÚde.

Table des matiĂšres



Section 0. RĂšgle d'or


0. RĂšgle d'or: respectez les tests Lean


Que faire. Le code de test est différent de ce qui est mis en service. Rendez-le aussi simple que possible, court, sans abstractions, célibataire, merveilleux au travail et économe. Une autre personne devrait regarder le test et comprendre immédiatement ce qu'il fait.

Nos tĂȘtes sont occupĂ©es par le code de production, elles n'ont pas d'espace libre pour une complexitĂ© supplĂ©mentaire. Si nous enfonçons une nouvelle partie de code complexe dans notre esprit, cela ralentira le travail de toute l'Ă©quipe sur une tĂąche pour laquelle nous testons. En fait, pour cette raison, de nombreuses Ă©quipes Ă©vitent simplement les tests.

Tests - c'est l'occasion d'avoir un assistant sympathique et souriant, avec qui il est trĂšs agrĂ©able de travailler et qui donne un Ă©norme retour sur de petits investissements. Les scientifiques pensent que dans notre cerveau, il existe deux systĂšmes: l'un pour les actions qui ne nĂ©cessitent pas d'effort, comme conduire sur une route vide, et le second pour les opĂ©rations complexes nĂ©cessitant une prise de conscience, telles que la rĂ©solution d'Ă©quations mathĂ©matiques. CrĂ©ez vos tests pour le premier systĂšme, de sorte que lorsque vous regardez le code, vous obtenez une sensation de simplicitĂ© comparable Ă  l'Ă©dition d'un document HTML, et non avec une solution 2X(17 × 24) .

Cela peut ĂȘtre rĂ©alisĂ© par une sĂ©lection rigoureuse des mĂ©thodes, des outils et des objectifs de test, afin qu'ils soient Ă©conomiques et offrent un retour sur investissement important. Testez seulement autant que nĂ©cessaire, essayez d'ĂȘtre flexible. Parfois, cela vaut mĂȘme la peine de rejeter certains tests et de sacrifier la fiabilitĂ© dans un souci de rapiditĂ© et de simplicitĂ©.



La plupart des recommandations ci-dessous découlent de ce principe.
Êtes-vous prĂȘt?

Section 1. Anatomie du test


1.1 Le nom de chaque essai doit ĂȘtre composĂ© de trois parties


Que faire. Le rapport de test doit indiquer si la rĂ©vision actuelle de l'application rĂ©pond aux exigences des personnes qui ne connaissent pas le code: les testeurs impliquĂ©s dans le dĂ©ploiement des ingĂ©nieurs DevOps, ainsi que vous-mĂȘme dans deux ans. Il sera prĂ©fĂ©rable que les tests fournissent des informations dans la langue des exigences et que leurs noms se composent de trois parties:

  1. Qu'est-ce qui est testé exactement? Par exemple, la méthode ProductsService.addNewProduct .
  2. Dans quelles conditions et scénarios? Par exemple, le prix n'est pas transmis à la méthode.
  3. Quel est le résultat attendu? Par exemple, un nouveau produit n'est pas approuvé.

Sinon. Le déploiement échoue, le test appelé «Ajouter un produit» échoue. Comprenez-vous exactement ce qui fonctionne mal?

Remarque Chaque chapitre a un exemple de code et parfois une illustration. Voir spoilers.

Exemples de code
Comment le faire correctement. Le nom du test se compose de trois parties.

 //1. unit under test describe('Products Service', function() { describe('Add new product', function() { //2. scenario and 3. expectation it('When no price is specified, then the product status is pending approval', ()=> { const newProduct = new ProductService().add(...); expect(newProduct.status).to.equal('pendingApproval'); }); }); }); 


1.2 Structurer les tests selon le modĂšle AAA


Que faire. Chaque test doit comprendre trois sections clairement séparées: organiser (préparation), agir (action) et affirmer (résultat). L'adhésion à une telle structure garantit que le lecteur de votre code n'a pas à utiliser le processeur du cerveau pour comprendre le plan de test:

Organiser: tout le code qui amÚne le systÚme à un état selon le scénario de test. Cela peut inclure la création d'une instance du module dans le concepteur de test, l'ajout d'enregistrements à la base de données, la création de stubs au lieu d'objets et tout autre code qui prépare le systÚme pour l'exécution du test.

Act: exécution de code dans le cadre d'un test. Habituellement, une seule ligne.
Affirmer: s'assurer que la valeur obtenue répond aux attentes. Habituellement, une seule ligne.

Sinon. Vous passerez non seulement de longues heures Ă  travailler avec le code principal, mais votre cerveau gonflera Ă©galement de ce qui devrait ĂȘtre un travail simple - des tests.

Exemples de code
Comment le faire correctement. Un test structuré selon le modÚle AAA.

 describe.skip('Customer classifier', () => { test('When customer spent more than 500$, should be classified as premium', () => { //Arrange const customerToClassify = {spent:505, joined: new Date(), id:1} const DBStub = sinon.stub(dataAccess, "getCustomer") .reply({id:1, classification: 'regular'}); //Act const receivedClassification = customerClassifier.classifyCustomer(customerToClassify); //Assert expect(receivedClassification).toMatch('premium'); }); }); 

Un exemple d'anti-modÚle. Aucune séparation, en un seul morceau, n'est plus difficile à interpréter.

 test('Should be classified as premium', () => { const customerToClassify = {spent:505, joined: new Date(), id:1} const DBStub = sinon.stub(dataAccess, "getCustomer") .reply({id:1, classification: 'regular'}); const receivedClassification = customerClassifier.classifyCustomer(customerToClassify); expect(receivedClassification).toMatch('premium'); }); 


1.3 Décrire les attentes dans la langue du produit: énoncer dans le style du BDD


Que faire. La programmation des tests dans un style déclaratif permet à l'utilisateur de comprendre immédiatement l'essence sans passer par un seul cycle du processeur cérébral. Lorsque vous écrivez du code impératif emballé dans une logique conditionnelle, le lecteur doit faire beaucoup d'efforts. De ce point de vue, vous devez décrire les attentes dans un langage de type humain dans un style BDD déclaratif en utilisant expect / should et non en utilisant du code personnalisé. Si dans Chai et Jest il n'y a pas d'assertion nécessaire, ce qui est souvent répété, alors vous pouvez étendre le matcher Jest ou écrire votre propre plugin pour Chai .

Sinon. L'équipe rédigera moins de tests et décorera les tests ennuyeux with .skip() .

Exemples de code
Un exemple utilisant Mocha .

Un exemple d'anti-modÚle. Pour comprendre l'essence du test, l'utilisateur est obligé de passer par un code impératif assez long.

 it("When asking for an admin, ensure only ordered admins in results" , ()={ //assuming we've added here two admins "admin1", "admin2" and "user1" const allAdmins = getUsers({adminOnly:true}); const admin1Found, adming2Found = false; allAdmins.forEach(aSingleUser => { if(aSingleUser === "user1"){ assert.notEqual(aSingleUser, "user1", "A user was found and not admin"); } if(aSingleUser==="admin1"){ admin1Found = true; } if(aSingleUser==="admin2"){ admin2Found = true; } }); if(!admin1Found || !admin2Found ){ throw new Error("Not all admins were returned"); } }); 

Comment le faire correctement. La lecture de ce test déclaratif est simple.

 it("When asking for an admin, ensure only ordered admins in results" , ()={ //assuming we've added here two admins const allAdmins = getUsers({adminOnly:true}); expect(allAdmins).to.include.ordered.members(["admin1" , "admin2"]) .but.not.include.ordered.members(["user1"]); }); 


1.4 Respecter les tests de la boßte noire: tester uniquement les méthodes publiques


Que faire. Tester l'intĂ©rieur entraĂźnera d'Ă©normes frais gĂ©nĂ©raux et ne rapportera presque rien. Si votre code ou votre API fournit les rĂ©sultats corrects, cela vaut-il la peine de passer trois heures Ă  tester COMMENT il fonctionne en interne, puis Ă  prendre en charge ces tests fragiles? Lorsque vous vĂ©rifiez le comportement public, vous vĂ©rifiez simultanĂ©ment implicitement l'implĂ©mentation elle-mĂȘme, vos tests Ă©chouent uniquement en cas de problĂšme spĂ©cifique (par exemple, sortie incorrecte). Cette approche est Ă©galement appelĂ©e test comportemental. D'un autre cĂŽtĂ©, si vous testez les composants internes (la mĂ©thode de la «boĂźte blanche»), alors au lieu de planifier la sortie des composants, vous vous concentrerez sur les petits dĂ©tails, et vos tests peuvent se casser en raison de petites modifications du code, mĂȘme si les rĂ©sultats sont corrects, mais l'escorte prendra beaucoup plus de ressources.

Sinon. Vos tests se comporteront comme un garçon criant "Loup!" : Signalez fortement les faux positifs (par exemple, le test échoue en raison d'un changement de nom d'une variable privée). Il n'est pas surprenant que bientÎt les gens commencent à ignorer les notifications CI, et un jour ils manqueront un vrai bug ...

Exemples de code
Un exemple d'anti-modÚle. tester l'intérieur sans raison valable.

Un exemple utilisant Mocha .

 class ProductService{ //this method is only used internally //Change this name will make the tests fail calculateVAT(priceWithoutVAT){ return {finalPrice: priceWithoutVAT * 1.2}; //Change the result format or key name above will make the tests fail } //public method getPrice(productId){ const desiredProduct= DB.getProduct(productId); finalPrice = this.calculateVATAdd(desiredProduct.price).finalPrice; } } it("White-box test: When the internal methods get 0 vat, it return 0 response", async () => { //There's no requirement to allow users to calculate the VAT, only show the final price. Nevertheless we falsely insist here to test the class internals expect(new ProductService().calculateVATAdd(0).finalPrice).to.equal(0); }); 


1.5 Choisissez la bonne mise en Ɠuvre simulĂ©e: Ă©vitez les faux objets au profit des talons et des espions


Que faire. Les implémentations simulées (test double) sont un mal nécessaire car elles sont associées aux internes de l'application, et certaines sont de grande valeur ( rafraßchissent la mémoire des implémentations imitées: faux objets (mocks), stubs (stubs) et objets espions (espions) ) Cependant, toutes les techniques ne sont pas équivalentes. Les espions et les talons sont conçus pour tester les exigences, mais ont un effet secondaire inévitable - ils affectent également légÚrement l'intérieur. Et les faux objets sont conçus pour tester l'intérieur, ce qui entraßne d'énormes frais généraux, comme décrit au chapitre 1.4.

Avant d'utiliser des implémentations simulées, posez-vous la question la plus simple: "Est-ce que je l'utilise pour tester des fonctionnalités qui sont apparues ou peuvent apparaßtre dans la documentation avec les exigences?" Sinon, ça sent les tests en boßte blanche.

Par exemple, si vous souhaitez savoir si l'application se comporte comme elle le devrait lorsque le service de paiement n'est pas disponible, vous pouvez crĂ©er un talon Ă  la place et retourner «Pas de rĂ©ponse» pour vĂ©rifier si le module testĂ© teste la bonne valeur. Vous pouvez donc vĂ©rifier le comportement / la rĂ©ponse / la sortie de l'application dans certains scĂ©narios. Vous pouvez Ă©galement confirmer avec l'aide d'un espion que lorsque le service n'Ă©tait pas disponible, la lettre a Ă©tĂ© envoyĂ©e, il s'agit Ă©galement d'un test de comportement, qui est mieux reflĂ©tĂ© dans la documentation avec les exigences ("Envoyer une lettre si les informations de paiement ne peuvent pas ĂȘtre enregistrĂ©es"). Dans le mĂȘme temps, si vous effectuez un faux service de paiement et assurez-vous qu'il est appelĂ© en utilisant les bons types JS, votre test vise les internes qui ne sont pas liĂ©s Ă  la fonctionnalitĂ© de l'application et qui sont susceptibles de changer souvent.

Sinon. Toute refactorisation de code implique la recherche et la mise Ă  jour de tous les faux objets dans le code. Les tests d'un ami assistant se transforment en fardeau.

Exemples de code
Un exemple d'anti-modĂšle. Les faux objets sont pour les tripes.

Exemple utilisant Sinon .

 it("When a valid product is about to be deleted, ensure data access DAL was called once, with the right product and right config", async () => { //Assume we already added a product const dataAccessMock = sinon.mock(DAL); //hmmm BAD: testing the internals is actually our main goal here, not just a side-effect dataAccessMock.expects("deleteProduct").once().withArgs(DBConfig, theProductWeJustAdded, true, false); new ProductService().deletePrice(theProductWeJustAdded); mock.verify(); }); 

Comment le faire correctement. Les espions sont conçus pour tester les exigences, mais il y a un effet secondaire - ils affectent inévitablement l'intérieur.

 it("When a valid product is about to be deleted, ensure an email is sent", async () => { //Assume we already added here a product const spy = sinon.spy(Emailer.prototype, "sendEmail"); new ProductService().deletePrice(theProductWeJustAdded); //hmmm OK: we deal with internals? Yes, but as a side effect of testing the requirements (sending an email) }); 


1.6 N'utilisez pas «foo», utilisez une entrée réaliste


Que faire. Souvent, des bogues de production se produisent avec des donnĂ©es d'entrĂ©e trĂšs spĂ©cifiques et surprenantes. Plus les donnĂ©es sont rĂ©alistes pendant les tests, plus elles sont susceptibles de dĂ©tecter des bogues Ă  temps. Pour gĂ©nĂ©rer des donnĂ©es pseudo-rĂ©elles qui simulent la variĂ©tĂ© et le type de donnĂ©es de production, utilisez des bibliothĂšques spĂ©ciales, par exemple, Faker . Ces bibliothĂšques peuvent gĂ©nĂ©rer des numĂ©ros de tĂ©lĂ©phone rĂ©alistes, des surnoms d'utilisateurs, des cartes bancaires, des noms de sociĂ©tĂ©s, voire le texte «lorem ipsum». Vous pouvez crĂ©er des tests (en plus des tests unitaires, et non Ă  leur place) qui randomisent de fausses donnĂ©es pour adapter le module Ă  un test, ou mĂȘme importer des donnĂ©es rĂ©elles Ă  partir d'un environnement de production. Envie d'aller encore plus loin? Lisez le chapitre suivant (sur les tests basĂ©s sur les propriĂ©tĂ©s).

Sinon. Vos tests de développement seront réussis en utilisant des entrées synthétiques comme «Foo», et les données de production peuvent se bloquer lorsqu'un pirate @3e2ddsf . ##' 1 fdsfds . fds432 AAAA ligne délicate comme @3e2ddsf . ##' 1 fdsfds . fds432 AAAA @3e2ddsf . ##' 1 fdsfds . fds432 AAAA @3e2ddsf . ##' 1 fdsfds . fds432 AAAA .

Exemples de code
Un exemple d'anti-modÚle. Une suite de tests qui s'exécute avec succÚs en raison de l'utilisation de données irréalistes.

Un exemple utilisant Jest .

 const addProduct = (name, price) =>{ const productNameRegexNoSpace = /^\S*$/;//no white-space allowed if(!productNameRegexNoSpace.test(name)) return false;//this path never reached due to dull input //some logic here return true; }; test("Wrong: When adding new product with valid properties, get successful confirmation", async () => { //The string "Foo" which is used in all tests never triggers a false result const addProductResult = addProduct("Foo", 5); expect(addProductResult).to.be.true; //Positive-false: the operation succeeded because we never tried with long //product name including spaces }); 

Comment le faire correctement. Rendre aléatoire une entrée réaliste.

 it("Better: When adding new valid product, get successful confirmation", async () => { const addProductResult = addProduct(faker.commerce.productName(), faker.random.number()); //Generated random input: {'Sleek Cotton Computer', 85481} expect(addProductResult).to.be.true; //Test failed, the random input triggered some path we never planned for. //We discovered a bug early! }); 


1.7 Utiliser des tests basés sur les propriétés pour valider plusieurs combinaisons d'entrées


Que faire. Habituellement, pour chaque test, nous sĂ©lectionnons plusieurs Ă©chantillons de donnĂ©es d'entrĂ©e. MĂȘme si le format d'entrĂ©e est similaire aux donnĂ©es rĂ©elles (voir le chapitre «Ne pas utiliser« foo »), nous ne couvrons que quelques combinaisons de donnĂ©es d'entrĂ©e (mĂ©thode ('', true, 1) , mĂ©thode ("string" , false" , 0) Mais en fonctionnement, une API appelĂ©e avec cinq paramĂštres peut ĂȘtre appelĂ©e avec des milliers de combinaisons diffĂ©rentes, dont l'une peut entraĂźner un plantage du processus ( fuzzing ). Et si vous pouviez Ă©crire un test qui envoie automatiquement 1000 combinaisons de donnĂ©es d'entrĂ©e et fixant, Ă  quelles combinaisons le code ne renvoie pas la bonne rĂ©ponse? La mĂȘme chose que nous faisons avec m todike test basĂ© sur les propriĂ©tĂ©s: en envoyant toutes les combinaisons possibles de donnĂ©es d'entrĂ©e dans l'unitĂ© de test , nous augmenter les chances d'une dĂ©tection de bogue, par exemple, nous avons une mĂ©thode. addNewProduct(id, name, isDiscount) Soutenir sa bibliothĂšque appellera cette mĂ©thode avec un certain nombre de combinaisons. (, , ) , par exemple, (1, "iPhone", false) , (2, "Galaxy", true) , etc. Vous pouvez tester en fonction des propriĂ©tĂ©s Ă  l'aide de votre lanceur de test prĂ©fĂ©rĂ© (Mocha, Jest etc.) et des bibliothĂšques comme js-verify ou testcheck (il a une bien meilleure documentation). Vous pouvez Ă©galement essayer la bibliothĂšque de vĂ©rification rapide , qui offre des fonctionnalitĂ©s supplĂ©mentaires et est activement accompagnĂ©e par l'auteur.

Sinon. Vous choisissez sans réfléchir les données d'entrée pour le test, qui ne couvre que les chemins d'exécution de code qui fonctionnent bien. Malheureusement, cela réduit l'efficacité des tests comme moyen de détecter les erreurs.

Exemples de code
Comment le faire correctement. Testez de nombreuses combinaisons avec le test moka.

 require('mocha-testcheck').install(); const {expect} = require('chai'); const faker = require('faker'); describe('Product service', () => { describe('Adding new', () => { //this will run 100 times with different random properties check.it('Add new product with random yet valid properties, always successful', gen.int, gen.string, (id, name) => { expect(addNewProduct(id, name).status).to.equal('approved'); }); }) }); 


1.8 Si nécessaire, utilisez uniquement des plans courts et en ligne.


Que faire. Lorsque vous devez effectuer un test basé sur des instantanés , utilisez uniquement des instantanés courts sans tout le supplément (par exemple, sur 3 à 7 lignes), en les incluant dans le cadre du test ( instantané en ligne ), et non en tant que fichiers externes. En suivant cette recommandation, vos tests resteront évidents et plus fiables.

D'un autre cÎté, les guides et outils «instantanés classiques» nous poussent à stocker de gros fichiers (par exemple, le balisage pour le rendu des composants ou les résultats de l'API JSON) sur des supports externes et à comparer les résultats avec la version enregistrée à chaque exécution du test. Il peut, par exemple, associer implicitement notre test à 1 000 lignes contenant 3 000 valeurs que l'auteur du test n'a jamais vues et auxquelles il ne s'attendait pas. Pourquoi est-ce mauvais? Parce qu'il y a 1000 raisons pour lesquelles le test échoue. Une seule ligne peut invalider un instantané, ce qui peut se produire souvent. Combien AprÚs chaque espace, commentaire ou modification mineure dans CSS ou HTML. De plus, le nom du test ne vous informera pas de l'échec, car il vérifie seulement que 1000 lignes n'ont pas changé, et encourage également l'auteur du test à prendre aussi longtemps que souhaité un long document qu'il n'a pas pu analyser et vérifier. Ce sont tous les symptÎmes d'un test obscur et hùtif qui n'a pas de tùche claire et essaie de faire trop.

Il convient de noter qu'il existe plusieurs situations dans lesquelles il est acceptable d'utiliser des images longues et externes, par exemple, pour confirmer le schéma, et non les données (extraire des valeurs et se concentrer sur les champs), ou lorsque les documents reçus changent rarement.

Sinon. Les tests d'interface utilisateur Ă©chouent. Le code semble bien, les pixels idĂ©aux sont affichĂ©s Ă  l'Ă©cran, alors que se passe-t-il? Vos tests avec des instantanĂ©s ont juste rĂ©vĂ©lĂ© la diffĂ©rence entre le document original et celui qui vient d'ĂȘtre reçu - un caractĂšre d'espace a Ă©tĂ© ajoutĂ© au balisage ...

Exemples de code
Un exemple d'anti-modĂšle. Associer un test Ă  quelques 2000 lignes de code inconnues.

 it('TestJavaScript.com is renderd correctly', () => { //Arrange //Act const receivedPage = renderer .create( <DisplayPage page = "http://www.testjavascript.com" > Test JavaScript < /DisplayPage>) .toJSON(); //Assert expect(receivedPage).toMatchSnapshot(); //We now implicitly maintain a 2000 lines long document //every additional line break or comment - will break this test }); 

Comment le faire correctement. Les attentes sont visibles et Ă  l'honneur.

 it('When visiting TestJavaScript.com home page, a menu is displayed', () => { //Arrange //Act receivedPage tree = renderer .create( <DisplayPage page = "http://www.testjavascript.com" > Test JavaScript < /DisplayPage>) .toJSON(); //Assert const menu = receivedPage.content.menu; expect(menu).toMatchInlineSnapshot(` <ul> <li>Home</li> <li> About </li> <li> Contact </li> </ul> `); }); 


1.9 Évitez les bancs de test globaux et les donnĂ©es initiales, ajoutez des donnĂ©es Ă  chaque test sĂ©parĂ©ment


Que faire. Selon la rÚgle d'or (chapitre 0), chaque test doit ajouter et travailler dans son propre ensemble de lignes dans la base de données afin d'éviter les liaisons, et il était plus facile pour les utilisateurs de comprendre le test. En réalité, les testeurs violent souvent cette rÚgle, avant d'exécuter des tests remplissant la base de données avec des données initiales (graines) ( également appelées «banc d'essai» ) afin d'augmenter la productivité. , (. « »), . . , , (, ).

. , , , ? , , , .

Un exemple d'anti-modÚle. Les tests ne sont pas indépendants et utilisent une sorte de hook global pour obtenir des données globales de la base de données.

 before(() => { //adding sites and admins data to our DB. Where is the data? outside. At some external json or migration framework await DB.AddSeedDataFromJson('seed.json'); }); it("When updating site name, get successful confirmation", async () => { //I know that site name "portal" exists - I saw it in the seed files const siteToUpdate = await SiteService.getSiteByName("Portal"); const updateNameResult = await SiteService.changeName(siteToUpdate, "newName"); expect(updateNameResult).to.be(true); }); it("When querying by site name, get the right site", async () => { //I know that site name "portal" exists - I saw it in the seed files const siteToCheck = await SiteService.getSiteByName("Portal"); expect(siteToCheck.name).to.be.equal("Portal"); //Failure! The previous test change the name :[ }); 

Comment le faire correctement. Vous pouvez rester dans le test, chaque test ne fonctionne qu'avec ses propres données.

 it("When updating site name, get successful confirmation", async () => { //test is adding a fresh new records and acting on the records only const siteUnderTest = await SiteService.addSite({ name: "siteForUpdateTest" }); const updateNameResult = await SiteService.changeName(siteUnderTest, "newName"); expect(updateNameResult).to.be(true); }); 


1.10 ,


. , - , try-catch-finally , . ( ), .

Chai: expect(method).to.throw ( Jest: expect(method).toThrow() ). , , . , , .

. (, CI-) , .

Un exemple d'anti-modÚle. Un cas de test long qui tente de détecter une erreur à l'aide de try-catch.

 /it("When no product name, it throws error 400", async() => { let errorWeExceptFor = null; try { const result = await addNewProduct({name:'nest'});} catch (error) { expect(error.code).to.equal('InvalidInput'); errorWeExceptFor = error; } expect(errorWeExceptFor).not.to.be.null; //if this assertion fails, the tests results/reports will only show //that some value is null, there won't be a word about a missing Exception }); 

. , , , QA .

 it.only("When no product name, it throws error 400", async() => { expect(addNewProduct)).to.eventually.throw(AppError).with.property('code', "InvalidInput"); }); 


1.11


. :

  • smoke-,
  • IO-less,
  • , , ,
  • , pull request', .

, , , #cold #api #sanity. . , Mocha : mocha — grep 'sanity' .

. , , , , , , , .

. '#cold-test' (Cold=== , - , ).

 //this test is fast (no DB) and we're tagging it correspondingly //now the user/CI can run it frequently describe('Order service', function() { describe('Add new order #cold-test #sanity', function() { it('Scenario - no currency was supplied. Expectation - Use the default currency #sanity', function() { //code logic here }); }); }); 


1.12


. , Node.js . , Node.

TDD . , , , . -- , - . , , . , . , , , , (, ..).

. , .

2:


2.1 :


. 10 , . . , 10 (, , ), , , ? ?

: 2019- , TDD , , , . , , , . IoT-, Kafka RabbitMQ, , - . , , ? (, , ), , - .

( ) , , (« API, , !» (consumer-driven contracts)). , : , , , .

: TDD - . TDD , . , .

. ROI, Fuzz, , 10 .

. Cindy Sridharan 'Testing Microservices — the sane way'



Un exemple:



2.2


. , . , . , , ? — . : TDD-, .

«», API, , (, , in-memory ), , , . , , « », .

. , , 20 %.

. Express API ( ).



2.3 , API


. , ( ). - , ! — , , . « -22 » : , , . (consumer-driven contracts) PACT : , 
 ! PACT — «», . PACT- — . , API CI, .

. — .

.



2.4


. , Express-. . , , , JS- {req,res}. , (, Sinon ) {req,res}, , . node-mock-http {req,res} . , , HTTP-, res- (. ).

. Express- === .

. , Express-.

 //the middleware we want to test const unitUnderTest = require('./middleware') const httpMocks = require('node-mocks-http'); //Jest syntax, equivalent to describe() & it() in Mocha test('A request without authentication header, should return http status 403', () => { const request = httpMocks.createRequest({ method: 'GET', url: '/user/42', headers: { authentication: '' } }); const response = httpMocks.createResponse(); unitUnderTest(request, response); expect(response.statusCode).toBe(403); }); 


2.5


. . CI- , . (, ), (, ), . Sonarqube (2600+ ) Code Climate (1500+ ). :: Keith Holliday

. , .

. CodeClimate, :



2.6 , Node


. , . ( ) . , - , ? ? , API 50 % ? , Netflix - ( Chaos Engineering ). : , . , Netflix, chaos monkey , , , , - ( Kubernetes kube-monkey , ). , . , , Node- , , v8 1,7 , UX , ? node-chaos (-), , Node.

. , production .

. Node-chaos , Node.js, .



2.7 ,


. ( 0), , , . , (seeds) ( « » ) . , (. « »), , . . , , (, ).

. , , , ? , , , .

. - .

 before(() => { //adding sites and admins data to our DB. Where is the data? outside. At some external json or migration framework await DB.AddSeedDataFromJson('seed.json'); }); it("When updating site name, get successful confirmation", async () => { //I know that site name "portal" exists - I saw it in the seed files const siteToUpdate = await SiteService.getSiteByName("Portal"); const updateNameResult = await SiteService.changeName(siteToUpdate, "newName"); expect(updateNameResult).to.be(true); }); it("When querying by site name, get the right site", async () => { //I know that site name "portal" exists - I saw it in the seed files const siteToCheck = await SiteService.getSiteByName("Portal"); expect(siteToCheck.name).to.be.equal("Portal"); //Failure! The previous test change the name :[ }); 

. , .

 it("When updating site name, get successful confirmation", async () => { //test is adding a fresh new records and acting on the records only const siteUnderTest = await SiteService.addSite({ name: "siteForUpdateTest" }); const updateNameResult = await SiteService.changeName(siteUnderTest, "newName"); expect(updateNameResult).to.be(true); }); 


3:


3.1. UI


. , , , . , , ( HTML CSS) . , (, , , ), , , .

. 10 , 500 (100 = 1 ) - - .

. .

 test('When users-list is flagged to show only VIP, should display only VIP members', () => { // Arrange const allUsers = [ { id: 1, name: 'Yoni Goldberg', vip: false }, { id: 2, name: 'John Doe', vip: true } ]; // Act const { getAllByTestId } = render(<UsersList users={allUsers} showOnlyVIP={true}/>); // Assert - Extract the data from the UI first const allRenderedUsers = getAllByTestId('user').map(uiElement => uiElement.textContent); const allRealVIPUsers = allUsers.filter((user) => user.vip).map((user) => user.name); expect(allRenderedUsers).toEqual(allRealVIPUsers); //compare data with data, no UI here }); 

. UI .
 test('When flagging to show only VIP, should display only VIP members', () => { // Arrange const allUsers = [ {id: 1, name: 'Yoni Goldberg', vip: false }, {id: 2, name: 'John Doe', vip: true } ]; // Act const { getAllByTestId } = render(<UsersList users={allUsers} showOnlyVIP={true}/>); // Assert - Mix UI & data in assertion expect(getAllByTestId('user')).toEqual('[<li data-testid="user">John Doe</li>]'); }); 


3.2 HTML- ,


. HTML- , . , , CSS-. , 'test-id-submit-button'. . , , .

. , , . — , , Ajax . . , CSS 'thick-border' 'thin-border'

. , .

 // the markup code (part of React component) <b> <Badge pill className="fixed_badge" variant="dark"> <span data-testid="errorsLabel">{value}</span> <!-- note the attribute data-testid --> </Badge> </b> // this example is using react-testing-library test('Whenever no data is passed to metric, show 0 as default', () => { // Arrange const metricValue = undefined; // Act const { getByTestId } = render(<dashboardMetric value={undefined}/>); expect(getByTestId('errorsLabel')).text()).toBe("0"); }); 

. CSS-.

 <!-- the markup code (part of React component) --> <span id="metric" className="d-flex-column">{value}</span> <!-- what if the designer changes the classs? --> // this exammple is using enzyme test('Whenever no data is passed, error metric shows zero', () => { // ... expect(wrapper.find("[className='d-flex-column']").text()).toBe("0"); }); 


3.3


. , , . , , . , — - , (. « » ). (, ) , .

, : , . ( ) . , .

. , . ?

. .

 class Calendar extends React.Component { static defaultProps = {showFilters: false} render() { return ( <div> A filters panel with a button to hide/show filters <FiltersPanel showFilter={showFilters} title='Choose Filters'/> </div> ) } } //Examples use React & Enzyme test('Realistic approach: When clicked to show filters, filters are displayed', () => { // Arrange const wrapper = mount(<Calendar showFilters={false} />) // Act wrapper.find('button').simulate('click'); // Assert expect(wrapper.text().includes('Choose Filter')); // This is how the user will approach this element: by text }) 

. .

 test('Shallow/mocked approach: When clicked to show filters, filters are displayed', () => { // Arrange const wrapper = shallow(<Calendar showFilters={false} title='Choose Filter'/>) // Act wrapper.find('filtersPanel').instance().showFilters(); // Tap into the internals, bypass the UI and invoke a method. White-box approach // Assert expect(wrapper.find('Filter').props()).toEqual({title: 'Choose Filter'}); // what if we change the prop name or don't pass anything relevant? }) 


3.4 .


. (, ). (, setTimeOut ) , . (, Cypress cy.request('url') ), API, wait(expect(element)) @testing-library/DOM . , API, , . , , hurry-up the clock . — , , ( ). , , - npm- , , wait-for-expect .

. , . , . .

. E2E API (Cypress).

 // using Cypress cy.get('#show-products').click()// navigate cy.wait('@products')// wait for route to appear // this line will get executed only when the route is ready 

. , DOM- (@testing-library/dom).
 // @testing-library/dom test('movie title appears', async () => { // element is initially not present... // wait for appearance await wait(() => { expect(getByText('the lion king')).toBeInTheDocument() }) // wait for appearance and return the element const movie = await waitForElement(() => getByText('the lion king')) }) 

. .

 test('movie title appears', async () => { // element is initially not present... // custom wait logic (caution: simplistic, no timeout) const interval = setInterval(() => { const found = getByText('the lion king'); if(found){ clearInterval(interval); expect(getByText('the lion king')).toBeInTheDocument(); } }, 100); // wait for appearance and return the element const movie = await waitForElement(() => getByText('the lion king')) }) 


3.5.


. - , . , , . : pingdom , AWS CloudWatch gcp StackDriver , , SLA. , , (, lighthouse , pagespeed ), . — , : , (TTI) . , , , , , DOM, SSL . , CI, 247 CDN.

. , , , , - CDN.

. Lighthouse .



3.6 API


. ( 2), , , ( ). API (, Sinon , Test doubles ), API. . API , ( ). API, . , , API . , : .

. , API 100 , 20 .

. API-.

 // unit under test export default function ProductsList() { const [products, setProducts] = useState(false) const fetchProducts = async() => { const products = await axios.get('api/products') setProducts(products); } useEffect(() => { fetchProducts(); }, []); return products ? <div>{products}</div> : <div data-testid='no-products-message'>No products</div> } // test test('When no products exist, show the appropriate message', () => { // Arrange nock("api") .get(`/products`) .reply(404); // Act const {getByTestId} = render(<ProductsList/>); // Assert expect(getByTestId('no-products-message')).toBeTruthy(); }); 


3.7 ,


. E2E (end-to-end, ) UI (. 3.6). , , . , , - . , — (, ), . - , , UI- Cypress Pupeteer . , : 50 , , . 10 . , , , — . , .

. UI , , ( , UI) .

3.8


. , API , , . (before-all), - . , : . , . - API- . , . (, ), , , . , : , API (. 3.6).

. , 200 , 100 , 20 .

. (before-all), (before-each) (, Cypress).

Cypress .

 let authenticationToken; // happens before ALL tests run before(() => { cy.request('POST', 'http://localhost:3000/login', { username: Cypress.env('username'), password: Cypress.env('password'), }) .its('body') .then((responseFromLogin) => { authenticationToken = responseFromLogin.token; }) }) // happens before EACH test beforeEach(setUser => () { cy.visit('/home', { onBeforeLoad (win) { win.localStorage.setItem('token', JSON.stringify(authenticationToken)) }, }) }) 


3.9 smoke-,


. production- , , . , , , , . smoke- . production, , , . , smoke- , .

. , , production . /Payment.

. Smoke- .

 it('When doing smoke testing over all page, should load them all successfully', () => { // exemplified using Cypress but can be implemented easily // using any E2E suite cy.visit('https://mysite.com/home'); cy.contains('Home'); cy.contains('https://mysite.com/Login'); cy.contains('Login'); cy.contains('https://mysite.com/About'); cy.contains('About'); }) 


3.10


. , . «» , , , , . , ( ) , , -, , , . « », . . , Cucumber JavaScript . StoryBook UI- , (, , , ..) , . , , .

. , .

. cucumber-js.

 // this is how one can describe tests using cucumber: plain language that allows anyone to understand and collaborate Feature: Twitter new tweet I want to tweet something in Twitter @focus Scenario: Tweeting from the home page Given I open Twitter home Given I click on "New tweet" button Given I type "Hello followers!" in the textbox Given I click on "Submit" button Then I see message "Tweet saved" 

. Storybook , .



3.11


. , . , . , . , - . , . , , , . , - . UI « ». , (, wraith , PhantomCSS), . (, Applitools , Perci.io ) , , « » (, ), DOM/CSS, .

. , ( ) , ?

. : , .



. wraith UI.

 ​# Add as many domains as necessary. Key will act as a label​ domains: english: "http://www.mysite.com"​ ​# Type screen widths below, here are a couple of examples​ screen_widths: - 600​ - 768​ - 1024​ - 1280​ ​# Type page URL paths below, here are a couple of examples​ paths: about: path: /about selector: '.about'​ subscribe: selector: '.subscribe'​ path: /subscribe 


4:


4.1 (~80 %),


. — , . , . — (, ), . ? , 10-30 % . 100 % , . . , : Airbus, ; , 50 % . , , 80 % ( Fowler: «in the upper 80s or 90s» ), , , .

: (CI), ( Jest ) , . , . , ( ) — . , — , , . , .

. . , , . .

.



. ( Jest).



4.2 ,


. , . , , , , . , , - , . PricingCalculator , , , 10 000 
 , , . , . 80- , . : , , , . , - .

. , , , .

. ? , QA . : , - . , - API .




4.3


. : 100 %, . ? , , , . . - : , , , .

, . JavaScript- Stryker :

  1. « ». , newOrder.price===0 newOrder.price!=0 . «» .
  2. , , : , . , , .

, , , .

. , 85- 85 % .

. 100 %, 0 %.

 function addNewOrder(newOrder) { logger.log(`Adding new order ${newOrder}`); DB.save(newOrder); Mailer.sendMail(newOrder.assignee, `A new order was places ${newOrder}`); return {approved: true}; } it("Test addNewOrder, don't use such test names", () => { addNewOrder({asignee: "John@mailer.com",price: 120}); });//Triggers 100% code coverage, but it doesn't check anything 

. Stryker reports, , ().



4.4 -


. ESLint. , eslint-plugin-mocha , ( describe() ), , . eslint-plugin-jest , ( ).

. 90- , , , . , .

. , , .

 describe("Too short description", () => { const userToken = userService.getDefaultToken() // *error:no-setup-in-describe, use hooks (sparingly) instead it("Some description", () => {});//* error: valid-test-description. Must include the word "Should" + at least 5 words }); it.skip("Test name", () => {// *error:no-skipped-tests, error:error:no-global-tests. Put tests only under describe or suite expect("somevalue"); // error:no-assert }); it("Test name", () => {*//error:no-identical-title. Assign unique titles to tests }); 


5: CI


5.1 ,


. — . , . , ( !). , . ( ESLint standard Airbnb ), . , eslint-plugin-chai-expect , . Eslint-plugin-promise ( ). Eslint-plugin-security , DOS-. eslint-plugin-you-dont-need-lodash-underscore , , V8, , Lodash._map(
) .

. , , . Que se passe-t-il? , , . . , , .

. , . , ESLint production-.



5.2


. CI , , ..? , . Pourquoi? : (1) -> (2) -> (3) . , , .

, , , , - .

CI- ( , CircleCI local CLI ) . , wallaby , ( ) . npm- package.json, , (, , , ). (non-zero exit code) concurrently . — , npm run quality . githook ( husky ).

. , .

. Npm-, , , .

 "scripts": { "inspect:sanity-testing": "mocha **/**--test.js --grep \"sanity\"", "inspect:lint": "eslint .", "inspect:vulnerabilities": "npm audit", "inspect:license": "license-checker --failOn GPLv2", "inspect:complexity": "plato .", "inspect:all": "concurrently -c \"bgBlue.bold,bgMagenta.bold,yellow\" \"npm:inspect:quick-testing\" \"npm:inspect:lint\" \"npm:inspect:vulnerabilities\" \"npm:inspect:license\"" }, "husky": { "hooks": { "precommit": "npm run inspect:all", "prepush": "npm run inspect:all" } } 


5.3 production-


. — CI-. . — Docker-compose . (, ) production-. AWS Local AWS-. , serverless AWS SAM Faas-.

Kubernetes CI-, . , « Kubernetes» Minikube MicroK8s , , . « Kubernetes»: CI- (, Codefresh ) Kubernetes-, CI- ; .

. .

: CI-, Kubernetes- (Dynamic-environments Kubernetes )

 deploy: stage: deploy image: registry.gitlab.com/gitlab-examples/kubernetes-deploy script: - ./configureCluster.sh $KUBE_CA_PEM_FILE $KUBE_URL $KUBE_TOKEN - kubectl create ns $NAMESPACE - kubectl create secret -n $NAMESPACE docker-registry gitlab-registry --docker-server="$CI_REGISTRY" --docker-username="$CI_REGISTRY_USER" --docker-password="$CI_REGISTRY_PASSWORD" --docker-email="$GITLAB_USER_EMAIL" - mkdir .generated - echo "$CI_BUILD_REF_NAME-$CI_BUILD_REF" - sed -e "s/TAG/$CI_BUILD_REF_NAME-$CI_BUILD_REF/g" templates/deals.yaml | tee ".generated/deals.yaml" - kubectl apply --namespace $NAMESPACE -f .generated/deals.yaml - kubectl apply --namespace $NAMESPACE -f templates/my-sock-shop.yaml environment: name: test-for-ci 


5.4


. , , . , 500 , , . , CI- ( Jest , AVA Mocha ) , . CI- (!), . , CLI , , .

. — , .

. Mocha parallel Jest Mocha ( JavaScript Test-Runners Benchmark )



5.5


. , . 10 ? CI- npm- license check plagiarism check ( ), , , Stackoveflow .

. , , .

.
 //install license-checker in your CI environment or also locally npm install -g license-checker //ask it to scan all licenses and fail with exit code other than 0 if it found unauthorized license. The CI system should catch this failure and stop the build license-checker --summary --failOn BSD 



5.6


. , Express, . npm audit , snyk ( ). CI .

. . .

: NPM Audit



5.7


. package-lock.json Yarn npm ( ): . npm install npm update , . , — . , package.json ncu .

, . :

  • CI , , npm outdated npm-check-updates (ncu). .
  • , pull request' .

: ? , ( , eslint-scope ). « »: latest , , (, 1.3.1, — 1.3.8).

. , .

: , , ncu CI-.



5.8 CI-, Node


. , Node . , Node.

  1. . , Jenkins .
  2. Docker.
  3. , . . smoke-, (, , ) .
  4. , , , , , .
  5. , . , -. - ( ).
  6. . .
  7. , , .
  8. (, Docker-).
  9. , . node_modules .

. , .

5.9 : CI-, Node


. , , . Node, CI . , MySQL, Postgres. CI- «», MySQl, Postgres Node. , - (, ). CI, , .

. - ?

: Travis ( CI) Node.

 language: node_js node_js: - "7" - "6" - "5" - "4" install: - npm install script: - npm run test 

Source: https://habr.com/ru/post/fr466879/


All Articles