Starten met unit testen in Vue

Vue wordt door steeds meer developers gebruikt. Laravel geeft out of the box de mogelijkheid om gebruik te maken van Vue en Bootstrap, maar ook React of Angular developers kiezen steeds vaker voor Vue. Na het meewerken bij een aantal projecten waar VueJS wordt ingezet begin ik te merken dat er nauwelijks wordt getest. Meest genoemde redenen zijn dat het testen te veel tijd kost en dat unit testen in de frontend moeilijk is. Volgens een enkeling zelfs onmogelijk, want hoe kun je nou testen zonder browser?

Niets is minder waar, testen en Vue gaan goed samen en zal je nauwelijks extra tijd kosten. Je merkt als je veel VueJS tests schrijft dat je steeds betere code schrijft omdat je werkt in units en code al testbaar opstelt. In dit artikel zullen we een simpel VueJS component voorzien van een unit test. Er wordt in dit voorbeeld vanuit gegaan dat je test met Jasmine met ondersteuning van Karma.

Het component

In dit voorbeeld gaan we een test schrijven voor een eenvoudig Vue component: De trainingComponent. Dit component houdt een lijst bij met deelnemers en een voorzitter. Met 2 methoden kunnen we deelnemers toevoegen en verwijderen en zien welke deelnemers er zijn.

import Vue from 'vue';
import _ from 'underscore';

const trainingComponent = Vue.component('training-component', {
    template: '<div></div>',
    data: function() {
        return {
            members: [
                'Dave',
                'Linda'
            ],
            teacher: 'Bert'
        };
    },
    methods: {
        addMember: function(name) {
            if (this.members.indexOf(name) === -1) {
                this.members.push(name);
            }
        },
        removeMember: function(name) {
            this.members = _.without(this.members, name);
            document.querySelector(‘.member[name=’ + name + ’]’).remove();
        }
    }
});

Zoals je kunt zien is dit geen component dat echt gebruikt zal worden aangezien er twee simpele methoden ontwikkeld zijn.

Component laden

Voordat een component getest kan worden moeten we een minimaal bestand maken. In dit bestand worden Vue en het te testen component geïmporteerd. Daarnaast moeten we er voor zorgen dat het component wordt geladen. In de browser kan dit door het toevoegen van een html-element in de html, maar bij een test moeten we het component met Vue uitvoeren.

import Vue from 'vue';
import trainingComponent from './training';

Let vm;
describe('Training component', () => {
    beforeEach(() => {
        vm = new Vue(trainingComponent);
    });
});

In deze lege test geven we aan wat we testen: “Training component” en geven we aan dat voor elke test het trainings-component opnieuw laden. Dit doen we zodat we er zeker van kunnen zijn dat elke test geïsoleerd wordt uitgevoerd. Het aanroepen van methodes in de ene test zouden nooit invloed moeten hebben op het resultaat van een andere test. Een instantie van het Vue component houden we daarom bij in een globale variable “vm”.

Bepalen van de te testen units

Nu het component geladen wordt kan er worden getest. Bij unit testen willen we altijd een klein gedeelte van de code testen (een unit). Bij Vue zijn dit de methods gedefineerd bij methods, computed en de lifecycle hooks als mounted en created. Mocht je grote of complexe methoden hebben dan kun je er ook voor kiezen om de methode in verschillende units te verdelen.

In de code die we nu testen zijn er twee methods. Dat betekent dat we twee units gaan testen; addMember en removeMember. Elke nieuwe unit definiëren we net zoals de lege test met een describe. Binnen deze describe zetten we de naam van de te testen methode.

describe('addMember()', () => {
});
describe('removeMember()', () => {
});

Testpaden bepalen en testen

Elke unit bestaat uit een aantal regels code. Deze regels met code willen we met een test allemaal een keer laten uitvoeren. Soms komt het door states of condities dat niet alle code uitgevoerd wordt. In deze gevallen schrijven we een extra test om aan alle condities een keer te voldoen. Dit zorgt er dus voor dat elke methode meerdere tests zou moeten hebben.

if (this.members.indexOf(name) === -1) {
    this.members.push(name);
}

De bovenstaande code zal dus twee tests opleveren. Eén waar het if-pad wordt genomen, en de ander waar het lege else-pad wordt genomen.

it('should add a member to the member list', () => {
    expect(vm.members.length).toBe(2);
    vm.addMember('Ike');
    expect(vm.members.length).toBe(3);
});

Deze test werkt als volgt. We controleren hoeveel members in de training aanwezig zijn en zeggen dat we er twee verwachten. Dit doen we voordat we de methode die we testen uitvoeren. Dit doen we om er zeker van te zijn dat ons startpunt klopt. Ons startpunt is dat er twee deelnemers zijn. Als we naar de code kijken klopt dit want er zijn twee deelnemers: Dave en Linda. Vervolgens voegen we een nieuwe deelnemer toe en controleren we direct of er nu 3 deelnemers zijn. Mocht dat zo zijn dan weten we dat de method addMember() werkt.

Omdat de method een if-statement bevat betekent het dat er ook een alternatief pad in de code aanwezig is en dat we een extra test moeten schrijven. De if-statement controleert of de member niet al in de lijst staat en voegt alleen de member toe als de member niet aanwezig is. We kunnen dus controleren als we tweemaal dezelfde member toevoegen dat de member er maar eenmalig is toegevoegd. De test ziet er als volgt uit:

it('shouldn\'t add the same member twice', () => {
    expect(vm.members.length).toBe(2);
    vm.addMember('Ike');
    expect(vm.members.length).toBe(3);
    vm.addMember('Ike');
    expect(vm.members.length).toBe(3);
});

Deze test werkt als volgt. Bij het starten van de test gaan we er vanuit dat er twee deelnemers zijn. Omdat elke test geïsoleerd wordt uitgevoerd weten we dat dit klopt door in de code van de Vue-component te kijken. Vervolgens voegen we de nieuwe deelnemer Ike toe. Direct controleren we of Ike daadwerkelijk is toegevoegd door het aantal deelnemers te tellen. Vervolgens proberen we Ike opnieuw toe te voegen. Omdat Ike al in de deelnemers lijst staat verwachten we dat Ike niet opnieuw wordt toegevoegd. Daarom controleren we direct na het opnieuw uitvoeren van de methode of de deelnemerslijst niet is aangepast.

Testgrenzen

Bij het testen van jouw code is het belangrijk om alleen jouw code te testen. Als je bijvoorbeeld externe modules gebruikt kost het te veel tijd om deze modules ook te testen. Daarnaast mag je er vanuit gaan dat de schrijver van elke module zelf zijn code beschermt met unit tests. We testen daarom dus nooit of jQuery, underscore of andere libraries echt werken.

In de method tweede methode, de removeMember() methode wordt er gebruik gemaakt van de UnderscoreJS methode without. Deze methode wordt gebruikt maar hoeft niet apart getest te worden. We hoeven deze methode dus niet te vullen met veel verschillende type data om te controleren of het werkt. Wel gaan we deze methode met onze tests uitvoeren waardoor de methode wel wordt getest, maar we gaan niet alle grenzen opzoeken van de UnderscoreJS methode.

Naast externe modules testen we ook geen dom-manupliaties met de unit tests. Doe je bijvoorbeeld veel met jQuery of met native javascript om manupilaties uit te voeren dan is het nauwelijks goed te testen of alles goed werkt. Onze unit tests worden uitgevoerd door een headless-browser. Alle visuele verandering zijn daarmee niet of nauwelijks te testen. Daarnaast zijn andere type tests beter in het testen van UI veranderingen. Dit kan handmatig worden gedaan door de developer, tester en product owner. Of je zou de UI kunnen testen met tools zoals Cypress (e2e) of BackstopJs.

Omdat we UI veranderingen niet willen testen kunnen we de methodes mocken. Dat betekent dat we de implementatie nabootsen. Bijvoorbeeld door altijd het gewenste resultaat uit een methode terug te laten komen. In ons geval willen we methode querySelector() mocken. We vinden het niet interessant of deze methode daadwerkelijk door ons headless browser goed is geïmplementeerd, maar vinden het wel interessant dat onze code deze methode uitvoert wanneer nodig. Hier komen Jasmine-spies bij kijken. Met een Spy “bespioneer” je een methode en kun je daarna controleren of er aanroepen zijn geweest. Bij de method removeMember() ziet dat er als volgt uit.

it('shouldn\'t remove non existing members', () => {
    spyOn(document, 'querySelector').and.return({ remove: function() { } });
    expect(vm.members.length).toBe(2);
    vm.removeMember('David');
    expect(vm.members.length).toBe(1);
    expect(document.querySelector).toHaveBeenCalled();
});

Hier wordt er een spy gedefineerd voor de methode querySelectorAll() op het document-object. We willen alleen weten dat de remove is aangeroepen maar vinden het niet belangrijk of deze methode daadwerkelijk werkt. We hebben namelijk deze methode niet zelf gescheven. Na het verwijderen van de deelnemer David controleren we of David daadwerkelijk uit de deelnemer lijst is verdwenen en we controleren of er een dom-manuplitantie is uitgevoerd.