Automatické testování pomocí Mocha

Automatizované testování bude použito v dalších úlohách a je také široce používáno v reálných projektech.

Proč potřebujeme testy?

Když píšeme funkci, obvykle si dokážeme představit, co by měla dělat:jaké parametry dávají jaké výsledky.

Během vývoje můžeme funkci zkontrolovat jejím spuštěním a porovnáním výsledku s očekávaným. Můžeme to udělat například v konzoli.

Pokud je něco špatně – pak kód opravíme, spustíme znovu, zkontrolujeme výsledek – a tak dále, dokud to nebude fungovat.

Ale takové ruční „opakování“ jsou nedokonalé.

Při testování kódu ručním opakovaným spuštěním je snadné něco přehlédnout.

Například vytváříme funkci f . Napsal nějaký kód, testování:f(1) funguje, ale f(2) nefunguje. Opravili jsme kód a nyní f(2) funguje. Vypadá kompletní? Ale zapomněli jsme znovu otestovat f(1) . To může vést k chybě.

To je velmi typické. Když něco vyvíjíme, máme na paměti spoustu možných případů použití. Ale je těžké očekávat, že programátor je po každé změně všechny ručně zkontroluje. Takže je snadné opravit jednu věc a rozbít jinou.

Automatizované testování znamená, že testy jsou psány odděleně, kromě kódu. Spouštějí naše funkce různými způsoby a porovnávají výsledky s očekávanými.a

Vývoj řízený chováním (BDD)

Začněme s technikou nazvanou Behavior Driven Development nebo zkráceně BDD.

BDD jsou tři věci v jednom:testy A dokumentace A příklady.

Abychom porozuměli BDD, prozkoumáme praktický případ vývoje.

Vývoj „pow“:specifikace

Řekněme, že chceme vytvořit funkci pow(x, n) což vyvolává x na celé číslo n . Předpokládáme, že n≥0 .

Tento úkol je jen příklad:existuje ** operátor v JavaScriptu, který to dokáže, ale zde se soustředíme na vývojový tok, který lze použít i na složitější úlohy.

Před vytvořením kódu pow , dokážeme si představit, co by funkce měla dělat a popsat ji.

Takový popis se nazývá specifikace nebo, stručně řečeno, specifikace a obsahuje popisy případů použití spolu s testy pro ně, jako je tento:

describe("pow", function() {

 it("raises to n-th power", function() {
 assert.equal(pow(2, 3), 8);
 });

});

Specifikace má tři hlavní stavební bloky, které můžete vidět výše:

describe("title", function() { ... })

Jaké funkce popisujeme? V našem případě popisujeme funkci pow . Používá se ke seskupování „pracovníků“ – it bloky.

it("use case description", function() { ... })

V názvu it jsme člověkem čitelným způsobem popište konkrétní případ použití a druhý argument je funkce, která jej testuje.

assert.equal(value1, value2)

Kód uvnitř it blok, pokud je implementace správná, by měl být proveden bez chyb.

Funkce assert.* se používají ke kontrole, zda pow funguje podle očekávání. Právě zde používáme jeden z nich – assert.equal , porovnává argumenty a dává chybu, pokud nejsou stejné. Zde zkontroluje, že výsledek pow(2, 3) rovná se 8 . Existují další typy srovnání a kontrol, které přidáme později.

Specifikace může být provedena a spustí test uvedený v it blok. To uvidíme později.

Tok vývoje

Tok vývoje obvykle vypadá takto:

  1. Je napsána počáteční specifikace s testy nejzákladnějších funkcí.
  2. Je vytvořena počáteční implementace.
  3. Abychom ověřili, zda to funguje, spouštíme testovací rámec Mocha (další podrobnosti brzy), který spouští spec. I když funkce není dokončena, zobrazují se chyby. Provádíme opravy, dokud vše nefunguje.
  4. Nyní máme funkční počáteční implementaci s testy.
  5. Do specifikace jsme přidali další případy použití, pravděpodobně ještě nepodporované implementacemi. Testy začnou selhávat.
  6. Přejděte na 3 a aktualizujte implementaci, dokud testy neukážou žádné chyby.
  7. Opakujte kroky 3–6, dokud nebude funkce připravena.

Vývoj je tedy iterativní . Napíšeme specifikaci, implementujeme ji, zajistíme, že testy projdou, pak napíšeme další testy, ujistíme se, že fungují atd. Na konci máme funkční implementaci a její testy.

Podívejme se na tento vývojový tok v našem praktickém případě.

První krok je již dokončen:máme počáteční specifikaci pro pow . Nyní, než provedeme implementaci, pojďme použít několik knihoven JavaScriptu ke spuštění testů, abychom viděli, že fungují (všechny selžou).

Specifikace v akci

Zde v tutoriálu použijeme pro testy následující JavaScriptové knihovny:

  • Mocha – základní rámec:poskytuje běžné testovací funkce včetně describe a it a hlavní funkce, která spouští testy.
  • Chai – knihovna s mnoha tvrzeními. Umožňuje používat mnoho různých asercí, nyní potřebujeme pouze assert.equal .
  • Sinon – knihovnu pro špehování funkcí, emulaci vestavěných funkcí a další, budeme ji potřebovat mnohem později.

Tyto knihovny jsou vhodné pro testování v prohlížeči i na straně serveru. Zde zvážíme variantu prohlížeče.

Úplná stránka HTML s těmito frameworky a pow specifikace:

<!DOCTYPE html>
<html>
<head>
 <!-- add mocha css, to show results -->
 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.css">
 <!-- add mocha framework code -->
 <script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js"></script>
 <script>
 mocha.setup('bdd'); // minimal setup
 </script>
 <!-- add chai -->
 <script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js"></script>
 <script>
 // chai has a lot of stuff, let's make assert global
 let assert = chai.assert;
 </script>
</head>

<body>

 <script>
 function pow(x, n) {
 /* function code is to be written, empty now */
 }
 </script>

 <!-- the script with tests (describe, it...) -->
 <script src="test.js"></script>

 <!-- the element with id="mocha" will contain test results -->
 <div id="mocha"></div>

 <!-- run tests! -->
 <script>
 mocha.run();
 </script>
</body>

</html>

Stránku lze rozdělit do pěti částí:

  1. <head> – přidat knihovny a styly třetích stran pro testy.
  2. <script> s funkcí k testování, v našem případě – s kódem pro pow .
  3. Testy – v našem případě externí skript test.js který má describe("pow", ...) shora.
  4. Prvek HTML <div id="mocha"> bude použita Mocha k výstupu výsledků.
  5. Test se spouští příkazem mocha.run() .

Výsledek:

V tuto chvíli test selže, došlo k chybě. To je logické:máme prázdný funkční kód v pow , takže pow(2,3) vrátí undefined místo 8 .

Pro budoucnost si všimněme, že existuje více testovacích běžců na vysoké úrovni, jako je karma a další, kteří usnadňují automatické spouštění mnoha různých testů.

Počáteční implementace

Udělejme jednoduchou implementaci pow , aby testy prošly:

function pow(x, n) {
 return 8; // :) we cheat!
}

Páni, teď to funguje!

Vylepšení specifikace

To, co jsme udělali, je rozhodně podvod. Funkce nefunguje:pokus o výpočet pow(3,4) by poskytl nesprávný výsledek, ale testy prošly.

…Ale situace je zcela typická, v praxi se to stává. Testy projdou, ale funkce nefunguje správně. Naše specifikace je nedokonalá. Musíme k tomu přidat další případy použití.

Přidejme ještě jeden test, abychom ověřili, že pow(3, 4) = 81 .

Zde můžeme vybrat jeden ze dvou způsobů organizace testu:

  1. První varianta – přidejte ještě jeden assert do stejného it :

    describe("pow", function() {
    
     it("raises to n-th power", function() {
     assert.equal(pow(2, 3), 8);
     assert.equal(pow(3, 4), 81);
     });
    
    });
  2. Druhý – proveďte dva testy:

    describe("pow", function() {
    
     it("2 raised to power 3 is 8", function() {
     assert.equal(pow(2, 3), 8);
     });
    
     it("3 raised to power 4 is 81", function() {
     assert.equal(pow(3, 4), 81);
     });
    
    });

Hlavní rozdíl je v tom, že když assert spustí chybu, it blok okamžitě skončí. Takže v první variantě, pokud je první assert selže, pak už nikdy neuvidíme výsledek druhého assert .

Oddělování testů je užitečné, abyste získali více informací o tom, co se děje, takže druhá varianta je lepší.

A kromě toho je tu ještě jedno pravidlo, které je dobré dodržovat.

Jeden test ověří jednu věc.

Pokud se podíváme na test a vidíme v něm dvě nezávislé kontroly, je lepší jej rozdělit na dvě jednodušší.

Pokračujme tedy druhou variantou.

Výsledek:

Jak jsme mohli očekávat, druhý test selhal. Jistě, naše funkce vždy vrací 8 , zatímco assert očekává 81 .

Zlepšení implementace

Pojďme napsat něco reálnějšího, aby testy prošly:

function pow(x, n) {
 let result = 1;

 for (let i = 0; i < n; i++) {
 result *= x;
 }

 return result;
}

Abychom se ujistili, že funkce funguje dobře, otestujme ji na více hodnot. Místo psaní it bloky ručně, můžeme je vygenerovat v for :

describe("pow", function() {

 function makeTest(x) {
 let expected = x * x * x;
 it(`${x} in the power 3 is ${expected}`, function() {
 assert.equal(pow(x, 3), expected);
 });
 }

 for (let x = 1; x <= 5; x++) {
 makeTest(x);
 }

});

Výsledek:

Vnořený popis

Přidáme ještě další testy. Předtím si ale všimněme pomocné funkce makeTest a for by měly být seskupeny. Nebudeme potřebovat makeTest v jiných testech je potřeba pouze v for :jejich společným úkolem je zkontrolovat, jak pow zvyšuje do dané moci.

Seskupení se provádí pomocí vnořeného describe :

describe("pow", function() {

 describe("raises x to power 3", function() {

 function makeTest(x) {
 let expected = x * x * x;
 it(`${x} in the power 3 is ${expected}`, function() {
 assert.equal(pow(x, 3), expected);
 });
 }

 for (let x = 1; x <= 5; x++) {
 makeTest(x);
 }

 });

 // ... more tests to follow here, both describe and it can be added
});

Vnořený describe definuje novou „podskupinu“ testů. Ve výstupu můžeme vidět nadpisové odsazení:

V budoucnu můžeme přidat další it a describe na nejvyšší úrovni s vlastními pomocnými funkcemi neuvidí makeTest .

before/after a beforeEach/afterEach

Můžeme nastavit before/after funkce, které se provádějí před/po spuštění testů, a také beforeEach/afterEach funkce, které se spouštějí před/po každém it .

Například:

describe("test", function() {

 before(() => alert("Testing started – before all tests"));
 after(() => alert("Testing finished – after all tests"));

 beforeEach(() => alert("Before a test – enter a test"));
 afterEach(() => alert("After a test – exit a test"));

 it('test 1', () => alert(1));
 it('test 2', () => alert(2));

});

Průběžná sekvence bude:

Testing started – before all tests (before)
Before a test – enter a test (beforeEach)
1
After a test – exit a test (afterEach)
Before a test – enter a test (beforeEach)
2
After a test – exit a test (afterEach)
Testing finished – after all tests (after)
Otevřete příklad v karanténě.

Obvykle beforeEach/afterEach a before/after se používají k provedení inicializace, vynulování čítačů nebo k něčemu jinému mezi testy (nebo testovacími skupinami).

Rozšíření specifikace

Základní funkce pow je kompletní. První iterace vývoje je hotová. Až skončíme s oslavami a pitím šampaňského – pojďme dál a vylepšeme to.

Jak bylo řečeno, funkce pow(x, n) je určen pro práci s kladnými celočíselnými hodnotami n .

K označení matematické chyby funkce JavaScriptu obvykle vrací NaN . Udělejme totéž pro neplatné hodnoty n .

Nejprve přidejte chování do specifikace(!):

describe("pow", function() {

 // ...

 it("for negative n the result is NaN", function() {
 assert.isNaN(pow(2, -1));
 });

 it("for non-integer n the result is NaN", function() {
 assert.isNaN(pow(2, 1.5));
 });

});

Výsledek s novými testy:

Nově přidané testy selžou, protože je naše implementace nepodporuje. Tak se dělá BDD:nejprve napíšeme neúspěšné testy a poté pro ně vytvoříme implementaci.

Jiná tvrzení

Všimněte si prosím tvrzení assert.isNaN :zkontroluje NaN .

V Chai jsou také další tvrzení, například:

  • assert.equal(value1, value2) – kontroluje rovnost value1 == value2 .
  • assert.strictEqual(value1, value2) – kontroluje přísnou rovnost value1 === value2 .
  • assert.notEqual , assert.notStrictEqual – inverzní kontroly k výše uvedeným.
  • assert.isTrue(value) – zkontroluje, že value === true
  • assert.isFalse(value) – zkontroluje, že value === false
  • …úplný seznam je v dokumentech

Měli bychom tedy přidat pár řádků do pow :

function pow(x, n) {
 if (n < 0) return NaN;
 if (Math.round(n) != n) return NaN;

 let result = 1;

 for (let i = 0; i < n; i++) {
 result *= x;
 }

 return result;
}

Nyní to funguje, všechny testy prošly:

Otevřete úplný závěrečný příklad v karanténě.

Shrnutí

V BDD je na prvním místě specifikace, následovaná implementací. Na konci máme jak specifikaci, tak kód.

Specifikace lze použít třemi způsoby:

  1. Jako testy – zaručují, že kód funguje správně.
  2. Jako Dokumenty – názvy describe a it říct, co funkce dělá.
  3. Jako Příklady – testy jsou ve skutečnosti funkční příklady ukazující, jak lze funkci použít.

Díky této specifikaci můžeme bezpečně vylepšit, změnit nebo dokonce přepsat funkci od začátku a zajistit, aby stále fungovala správně.

To je zvláště důležité ve velkých projektech, kdy se funkce používá na mnoha místech. Když takovou funkci změníme, neexistuje žádný způsob, jak ručně zkontrolovat, zda každé místo, které ji používá, stále funguje správně.

Bez testů mají lidé dva způsoby:

  1. Chcete-li provést změnu, ať se děje cokoliv. A pak se naši uživatelé setkávají s chybami, protože pravděpodobně nedokážeme něco ručně zkontrolovat.
  2. Nebo, pokud je trest za chyby tvrdý, protože neexistují žádné testy, lidé se bojí takové funkce upravovat a kód pak zastará, nikdo se do něj nechce pouštět. Není dobré pro vývoj.

Automatické testování pomáhá těmto problémům předejít!

Pokud je projekt pokryt testy, žádný takový problém neexistuje. Po jakýchkoli změnách můžeme spustit testy a vidět spoustu kontrol provedených během několika sekund.

Kromě toho má dobře otestovaný kód lepší architekturu.

Přirozeně je to proto, že automaticky testovaný kód se snadněji upravuje a vylepšuje. Ale je tu ještě další důvod.

Pro psaní testů by měl být kód organizován tak, aby každá funkce měla jasně popsaný úkol, dobře definovaný vstup a výstup. To znamená dobrou architekturu od začátku.

V reálném životě to někdy není tak jednoduché. Někdy je obtížné napsat specifikaci před skutečný kód, protože ještě není jasné, jak se má chovat. Ale obecně psaní testů dělá vývoj rychlejší a stabilnější.

Později v tutoriálu se setkáte s mnoha úkoly se zapečenými testy. Takže uvidíte více praktických příkladů.

Psaní testů vyžaduje dobrou znalost JavaScriptu. Ale teprve se to začínáme učit. Abychom vše urovnali, od této chvíle nemusíte psát testy, ale měli byste je již umět číst, i když jsou trochu složitější než v této kapitole.