Testovanie Vue.js komponentov


Testovanie Vue.js komponentov

Pri budovaní komplexnejších aplikácií existuje veľa miest, kde sa môže niečo pokaziť. Vtedy je jedno, či ide o chybu vo frontende, backende, serveri,… aplikácia je v očiach klienta v momente nefunkčná a developerovi neostáva nič iné, ako pátrať po chybe. Mať dedikovaného testera by bolo určite super, ale pre niekoho to je až príliš veľký alebo aj zbytočný luxus. Veľa chýb je možné podchytiť pokrytím kódu automatizovanými testami.

Či už programuješ jednoduché Vue.js komponenty alebo celé JS aplikácie, určite sa stretneš so situáciou, kedy musíš rátať so spracovaním rôznych vstupov a zobrazovaním rôznych variant výstupov. Manuálnym testovaním pri každej zmene zabíjaš aj desiatky minút, ktoré by si vedel určite využiť efektívnejšie. Tvorba nepriestrelného kódu, ktorý za každých okolností robí to, čo má, je cieľom každého z nás. Tu na pomoc nastupuje Unit testovanie Vue.js komponentov.

Unit testy sú dôležitou časťou integračného procesu, aby si mal vo svoj kód stopercentnú dôveru a za každých okolností si bol istý, že žiadna zmena v kóde ti nezhodí dôležitú funkcionalitu v aplikácii.

Prejdem teraz základným setupom a ukážem na pár príkladoch základy testovania Vue.js komponentov. Nebudú chýbať ani tipy pre jednoduchšie písanie testov. V prípade, že sa ti článok bude páčiť alebo by si chcel prebrať v ďalšom článku aj testovanie iných častí Vue.js ekosystému, ako Vuex alebo Vue Router, pokojne napíš do komentára.

Inštalácia
Predpokladám, že máš vytvorenú Vue.js aplikáciu s webpack buildom. Ak nie, na začiatok stačí nainštalovať:

npm install --save vue
npm install --save-dev webpack webpack-cli

Aby webpack vedel spracovať .vue štruktúru (by default by sa spracovávala ako js súbor) Vue komponentov, potrebujem do webpack configu pridať vue-loader a ešte budem potrebovať compiler na Vue templaty.

npm install --save-dev vue-loader vue-template-compiler

Ďalším krokom je inštalácia vue-test-utils, mocha test frameworku a assert library expect.

npm install --save-dev @vue/test-utils mocha mocha-webpack@next
npm install --save-dev jsdom jsdom-global
npm install --save-dev expect

Môj webpack config momentálne vyzerá nasledovne:

const path = require('path');
const { VueLoaderPlugin } = require('vue-loader');
 
module.exports = {
  mode: 'development',
  entry: 'app.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  resolve: {
    alias: {
      'vue$': 'vue/dist/vue.esm.js' 
    }
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin()
  ]
}

Jediné, čo ma tu môže zaujímať, je použitie vue-loadera. Bez neho, prípadne bez žiadneho configu, webpack nebude vedieť, ako .vue súbor spracovať.

Testy sa spúšťajú príkazom:

mocha-webpack --webpack-config webpack.config.js --require mocha.setup.js test/**/*.spec.js

V mocha.setup.js môžem definovať globálne premenné, pomocné funkcie, kód ktorý by sa mal spustiť na začiatku testovania a ďalšie featury, ktoré využijem pri písaní testov. V našom prípade ide o načítanie js toolu pre generovanie DOM – jsdom a spomenutej assert library – expect.

require('jsdom-global')();
 
global.expect = require('expect');

Základná štruktúra testov vyzerá takto:

import { mount } from '@vue/test-utils';
import MyComponent from './my-component.vue';
 
const wrapper = mount(MyComponent);
 
describe('MyComponent test', function() {
    // tests
});

Z vue-test-utils sa naimportuje metóda mount, ktorá v testoch vytvorí wrapper Vue komponentov, ktorý v izolácii vie nastavovať dáta, triggernúť eventy a renderovať HTML. V ukážke som vytvoril wrapper na importovaný my-component. V prípade, že testujem komponent, ktorý obsahuje množstvo child komponentov, ich opakované renderovanie môže spomaľovať celý testovací proces. Keď chcem testovať len konkrétny komponent, nezávislý na jeho childs, viem použiť namiesto mount metódu shallowMount, ktorá nerenderuje child komponenty.

Blok describe už predstavuje sadu testov. Jednotlivé testy sa potom píšu do blokov it.

TIP: Spustenie len jedného testu
Pri písaní testov nemusím čakať, kým mi zbehne celá sada testov. Celý proces môže trvať nejaké sekundy, prípadne až minúty, kým všetko dobehne a toto čakanie vie celkom potrápiť pri písaní väčšieho množstva testov, prípadne pri refaktoringu testov. V tejto ukážke bude vďaka flagu .only zbiehať len test „Third test“.

describe('MyComponent test', function() {
 
  it('First test', function () {});
 
  it('Second test', function () {});
 
  it.only('Third test', function () {});
 
});

Tento flag je v prípade potreby možné pridať aj na celú sadu testov – describe.only(‘TestSuite’, () => {}).

Môj prvý test
Predstavme si jednoduchý Vue komponent, ktorý nerobí nič iné, len vypisuje text, ktorého zmena v inpute sa ihneď reflektuje aj do contentu.

<template>
  <div>
    <p>Tvorba výkonných internetových obchodov od {{ companyName }}</p>
    <input type="text" v-model="companyName">
  </div>
</template>
 
<script>
  export default {
    data: function() {
      return {
        companyName: 'My company'
      }
    }
  }
</script>

Čo všetko chcem pri takejto funkcionalite vlastne testovať?

Na začiatok by som chcel mať istotu, že sa HTML kód vygeneroval správne. To viem napríklad overiť tým, že existuje input, do ktorého bude môcť užívateľ písať.

...
  it('Has a input', () => {
    expect(wrapper.contains('input')).toBe(true);
  });
...

Ďalej môžem otestovať, či sa zobrazuje správne defaultný text.

...
  it('Displays default text', function() {
    expect(wrapper.vm.$data.companyName).toBe('My company');
  });
...

A nakoniec, či komponent reflektuje zmeny v obsahu po editovaní inputu.

...
  it('Changed input change state & text', function() {
    const input = wrapper.find('input');
    let newText = 'RIESENIA.com';
 
    input.element.value = newText;
    input.trigger('input');
 
    expect(wrapper.vm.$data.companyName).toBe(newText);
    expect(wrapper.text()).toContain(newText);
  });
...

Spustením testov by som mal vidieť 0 chýb.
mocha success

V prípade chyby vidím presne, o akú chybu ide, aký test padol, aká bola očakávaná a reálna hodnota a tiež riadok, kde test padol.
mocha fail

TIP: Definovanie NPM skriptu pre spustenie testov
Aby som si nemusel pamätať a písať opakovane taký dlhý príkaz na spustenie testov, využijem package.json, konkrétne scripts property, v ktorej si môžem definovať vlastné skripty, ktoré potom volám cez npm run {názov_skriptu}.

...
"scripts": {
  "test": "mocha-webpack -w --webpack-config webpack.config.js --require test/setup.js test/**/*.spec.js"
}
...

Teraz testy môžem spustiť príkazom:

npm run test

Konečná podoba môjho package.json súboru je:

{
  "name": "vue-app",
  "version": "1.0.0",
  "description": "Best Vue.js application",
  "main": "index.js",
  "scripts": {
    "test": "mocha-webpack -w --webpack-config webpack.config.js --require mocha.setup.js test/**/*.spec.js"
  },
  "author": "riesenia.com",
  "license": "ISC",
  "devDependencies": {
    "@vue/test-utils": "^1.0.0-beta.29",
    "expect": "^24.1.0",
    "jsdom": "^13.2.0",
    "jsdom-global": "^3.0.2",
    "mocha": "^6.0.1",
    "mocha-webpack": "^2.0.0-beta.0",
    "vue-loader": "^15.6.4",
    "vue-template-compiler": "^2.6.7",
    "webpack": "^4.29.5",
    "webpack-cli": "^3.2.3"
  },
  "dependencies": {
    "vue": "^2.6.7"
  }
}

TIP: Generovanie nového komponentu pred každým testom
Pri definovaní wrappera ako v predošlom prípade, ovplyvňuje každý jeden test stav komponenty. Ak by som na konci spustil ešte jeden test správnosti nastavenia defaultného textu, dostal by som chybovú hlášku, pretože dáta boli v predošlom teste zmenené a môj nový test už pracuje s týmto stavom. Aby som mal istotu, že komponent má pred každým testom pôvodný stav, môžem presunúť inicializáciu wrappera do beforeEach bloku.

describe('MyComponent test', function() {
 
  beforeEach(function() {
    wrapper = mount(MyComponent);
  });
 
});

V prípade, že stav komponentu držím vo Vuex (tool pre state management), nemusím inicializovať celý wrapper, ale len definovať default stav.

Testovanie click eventov a viditeľnosti elementov
Mám komponent, ktorý rieši incrementovanie a decrementovanie hodnoty v stave, pričom ak bude hodnota rovná alebo vačšia 5, zobrazí sa button na decrementovanie a error hláška.

<template>
  <div>
    <p id="error" v-if="showAlert">Si doklikal!</p>
    <button id="incrementBtn" @click="increment">+1</button>
    <button id="decrementBtn" @click="decrement" v-show="showAlert">-1</button>
  </div>
</template>
 
<script>
  export default {
    data: function() {
      return {
        value: 0
      }
    },
    computed: {
      showAlert: function() {
        return this.value >= 5;
      }
    },
    methods: {
      increment: function() {
        this.value += 1;
      },
      decrement: function() {
        this.value -= 1;
      }
    }
  }
</script>

Správne vyrenderovaný komponent otestujem napríklad existenciou button na incrementovanie. To z predošlých testov už nie je žiadny problém. Prvou výzvou je testovanie existencie elementu.

...
  it('Alert message is not available', function () {
    expect(wrapper.find('#error').exists()).toBe(false);
  });
...

Ďalej chcem vedieť, či element je skutočne vyrenderovaný a existuje v HTML outpute, len nie je viditeľný.

...
  it('Has hidden decrement button', function() {
    expect(wrapper.find('#decrementBtn').exists()).toBe(true);
    expect(wrapper.find('#decrementBtn').isVisible()).toBe(false);
  });
...

TIP: Pomocné funkcie
Klikanie na button a assert správnosti value je už jednoduchý, preto si na ďalšom príklade ukážeme použitie pomocnej funkcie kliknutia na button s incrementovaním, ktorú potom môžem použiť v ďalších testoch a nemusím písať kód opakovane.

...
  it('Increments value', function () {
    expect(wrapper.vm.$data.value).toBe(0);
 
    increment(1);
 
    expect(wrapper.vm.$data.value).toBe(1);
  });
 
  let increment = function(times) {
    let incrementBtn = wrapper.find('#incrementBtn');
 
    for (var i = 0; i < times; i++) {
      incrementBtn.trigger('click');
    }
  };
...

Nakoniec ma zaujíma, či po dosiahnutí hodnoty 5 bude zobrazená error hláška a nový button. Posledným testom bude, či po dosiahnutí hodnoty 5 a kliknutí na #decrementBtn dostanem hodnotu o 1 nižšiu a elementy sa správne skryjú.

...
  it('Displays alert & decrement button', function () {
    expect(wrapper.vm.$data.value).toBe(0);
 
    increment(5);
 
    expect(wrapper.vm.$data.value).toBe(5);
    expect(wrapper.find('#error').exists()).toBe(true);
    expect(wrapper.find('#decrementBtn').isVisible()).toBe(true);
  });
 
  it('Hides alert & decrement button', function () {
    increment(5);
    decrement(1);
 
    expect(wrapper.vm.$data.value).toBe(4);
    expect(wrapper.find('#error').exists()).toBe(false);
    expect(wrapper.find('#decrementBtn').isVisible()).toBe(false);
  });
...

Testovanie funkcií s AJAX volaniami
Tu predpokladám, že aplikácia používa tool na volanie HTTP requestov axios.

npm install --save axios

Ak axios nepoznáš, odporúčam si pozrieť jeho dokumentáciu. Jednotlivé HTTP requesty vytváram takto:

// GET request
axios.get('/products')
  .then(function (response) {
    // handle success
    console.log(response);
  })
  .catch(function (error) {
    // handle error
    console.log(error);
  })
  .then(function () {
    // always executed
  });
 
// POST request
axios.post('/create-user', {
    firstName: 'Jozef',
    lastName: 'Pestorek'
  })
  .then(function (response) {
    // handle success
    console.log(response);
  })
  .catch(function (error) {
    // handle error
    console.log(error);
  });

Problémom je, že pri testovaní pracujem bez servera, na ktorý by som mohol requesty posielať. Ako potom otestujem, či môj asynchrónny kód pracuje ako má, naplní správne stav a HTML je korektne vyrenderované? Určite nechcem kód upravovať do takej miery, že mi vznikne „test“ verzia komponentu. Na mockovanie axios requestov v mocha testoch použijem tool moxios, ktorý vie tieto volania z komponentu zachytiť a pre testovacie účely mi celý response zo servera bude imitovať.

npm install --save-dev moxios

Ďalším krokom je inštalovanie moxios v moche. Rovno pridám ukážku, ako namockovať request, ktorý v response vracia pole produktov:

// other imports
import moxios from 'moxios';
 
describe.only('MyComponent test', function() {
    beforeEach(function () {
      // import and pass your custom axios instance to this method
      moxios.install()
    });
 
    afterEach(function () {
      // import and pass your custom axios instance to this method
      moxios.uninstall()
    });
 
    // tests
    it('fetch products from server', function (done) {
 
      moxios.stubRequest('/products', {
        status: 200,
        response: [
          {id: 1, name: 'iPhone 6'},
          {id: 2, name: 'iPhone 6s'},
          {id: 3, name: 'iPhone 7'}
        ]
      });
 
      moxios.wait(function() {
         // assertions here
 
         done();
      });
 
    });
 
});

Tu by som vypichol okrem príkazu moxios.stubRequest() (mockovanie requestu) 2 veci:
moxios.wait(), do ktorého budem písať jednotlivé asserty, ktoré sa spustia v callback funkcii po dobehnutí requestu
done(), ktorý oznámi moche, kedy môj asynchrónny test reálne skončil. Bez done() môj test skončí ako valídny, bez vykonania akéhokoľvek assertu. Môj kód je vďaka použitiu axios asynchrónny a test skončí skôr, než dobehne request a spustili sa asserty definované v callback funkcii.

TIP: Spustenie testov automaticky pred každým git push
Pokiaľ nemáš rozbehané žiadne CI, vieš si pomôcť lokálnym „CI“ v podobe GIT hooks. Ide o spúšťanie vlastného kódu pred špecifickými eventami GIT-u, ako pre-pull, pre-push, pre-commit atď. Pre uľahčenie práce s GIT hooks, používame tool husky, ktorému povieme, čo má spustiť v config súbore .huskyrc.

{
  "hooks": {
    "pre-push": "npm run test"
  }
}

Takto mám istotu, že do repozitára pôjde vždy otestovaný kód. Ďalej môžem hooks využiť napríklad na automatický lintering kódu, generovanie commit message atď.

Honorable mentions
Jest – test framework alternatíva
Chai – assertion library alternatíva
Should – assertion library alternatíva
Sinon – request mocks alternatíva

Záverom
Písanie testov je super vec, ale k tomu je potrebným predpokladom písanie testovateľného kódu. Často je devel flow taký, že najskôr sa rýchlo porieši funkcionalita a až potom príchádza rad na písanie testov. Prípadne sa tento step úplne vypustí kvôli nedostatku času, budgetu, kapacít, prípadne je napísaný kód netestovateľný a pokus o písanie testov je dosť výzva. Existujú metodiky pre písanie testov, ktoré mi vedia pomôcť s podobnými problémami a majú výborný efekt v tom, že budem mať oveľa vačšiu dôveru pri písaní novej funkcionality, respektíve refactoring už nebude toľko bolestivý. Takže neváhaj a testuj svoj kód! 🙂

+ Diskusia nemá žiadne príspevky