Mockito – ja testataan ihan mitä vain isolaatiossa

Olen joskus aiemmin kirjaillut juttuja siitä miten esim. EJB komponentteja voidaan yksikkötestata – ja käytellyt välineenä EasyMockia. Testaus isolaatiossa, eristyksissä muusta on yksi tavoiteltava asia koska näin tehdyt testit voidaan ajaa salamannopeasti ja jatkuvasti, saaden palautetta muutoksista koko ajan.

Mockito-JUnit

Sanoinko testit? Köhköh, tarkoitin tiestysti speksit. Isolaatiotestit mahdollistavat TDD ja BDD disipliinien soveltamisen haltuessaan, tai itseni suosima Spec-While-Writing tapa joka on oikeastaan ihan samaa kuin em mutta rennommin rantein. Fanaattiset isolaatiotestithän eivät nimenomaan ole testeinä kovinkaan hyviä usein, mutta spekseinä ne ovat aivan verrattomia, ja monimutkaiselle logiikalle lähes ilmaiseksi sivuvaikutuksena syntyvät testitkään eivät ole ihan huono asia.

Sanoinko ilmaiseksi? Hups. Kyllähän testien kirjoittamiseen aikaa kuluu, etenkin jos niitä säveltää etupainotteisesti ennen koodipyrähdyksiä. Etenkin isolaation saavuttaminen on aika ajoin hankalaa. Viime aikoina olen löytänyt uutta apua tälle saralle: Kuten otsikko jo spoilasi, Mockito framework on varsin kiva, etenkin Spring Bootin kanssa. Vastaavaan tapaan Javascript-puolella käyttelen nykyään Jasmine mockeja, mutta tämä ei ole artikkeli niistä vaan Mockitosta. Tässä siis vähän maistiaisia siitä miltä maistuu yksikkötestaus mockitolla ja mitä hyöytyä siitä saa.

Mockito testi voi olla tavallinen JUnit tai TestNG testi, eli aloitellaan tähän tapaan:

public class UserResourceTest {

  @Test
  public void changePasswordShouldSucceedWithValidParameters() 
    throws Exception {
      fail("Not implemented yet");
  }

}

Testatessa on tietysti mukavaa olla se mitä testataan. Perinteisesti sen voi vaikka instansioida nimellä sut – system under test, Mockitolla homma hoituu näin:

@InjectMocks
private UserResource sut;

Jotta tuo tekee jotain on tietysti syytä ajaa tämä Mockiton kera. Lisätäänpä siis testiluokan alkuun:

@RunWith(MockitoJUnitRunner.class)

Nyt tapahtuu jo jotain. Tässä testattavassa luokassa on kuitenkin paljon riippuvuuksia, jäsenmuuttujissa on yhtä ja toista jota Spring injektoi ajon aikana, mutta jotka aiheuttavat nullpointer exceptioneitä ellei niitä ole alustettu. Olen joskus injektoinut näitä riippuvuuksia reflectionin avulla, joskus setXXX-metodeita tekemällä. Mockitolla homma sujuu kokonaisuudessaan näin:

@RunWith(MockitoJUnitRunner.class)
public class UserResourceTest {

    @Mock
    private Utils utils;

    @Mock
    private UserDetailsService userDetailsServiceMock;

    @InjectMocks
    private UserResource sut;

Eli, kun alan kirjoittelemaan omaa testiäni, kaikki kolme edellämainittua on instansioitu Mockiton toimesta, mock objekteille on mock toteutukset, jotka esim. palauttavat null/eivät tee mitään. Ja mikä parasta, mock objektit on injektoitu sut objektin sisään, privaatteihin jäsenmuuttuja-kenttiin. Ensin yritetään samalla tyypillä, sitten samalla nimellä.

Mockiton oma dokumentaatio InjectMocks-metodista sanoo näin:

Mockito will try to inject mocks only either by constructor injection, setter injection, or property injection in order and as described below. If any of the following strategy fail, then Mockito won’t report failure; i.e. you will have to provide dependencies yourself.

Eli tarkkana tämän kanssa.

Mitä muuta hienoa? No Mockitossa on verrattain helppoa opettaa mockit palauttamaan sopivia arvoja, esim:

UserEntity currentUser = new UserEntity();
UserAuthentication userAuthentication = new UserAuthentication(currentUser);
when(utils.getAuthentication()).thenReturn(userAuthentication);

Ja sitten vain paukuttamaan omaa suttia, joka taas kutsuu tuota utils.getAuthentication() metodia. Jos haluat tarkistaa, kutsuiko, sen voi tarkistaa verify-kutsulla:

verify(utils).getAuthentication();

Eli, Mockito vaikuttaisi hyvältä. Pääidea on tuoda testauksen hintaa alas, ja näin saada siitä yhtä luontevaa kuin hengittäminen. Tai luontevampaa.

 

Mainokset

Angular testing cheat sheet

Sen verran useasti Angularin parissa tulee pyörittyä, että pidän seinälle tulostettuna muutamaa yleisintä testaukseen liittyvää jippoa. Javascript kun ei anna käännöksenaikaista palautetta, on erityisen tärkeää tietää API. Samoin liikkeellä on myös huonoa ja vanhentunutta tietoa; Yli puolet netin resursseista listailee vanhentuneita komentoja esim. jasminen ja protratorin rajapinnoista.

Pidemmittä puheitta siis, tässä Angular testauksen cheat sheet:

// jasmine matchers
describe('jasmine matchers', function() {
  it('demonstrate use of built-in matchers', function() {
    expect(true).toBeTruthy();
    expect(false).not.toBeTruthy();
    expect(false).toBeFalsy();
    expect(true).not.toBeFalsy();
    expect({}).toBeDefined();
    expect(undefined).not.toBeDefined();
    expect(null).toBeNull();
    expect(undefined).not.toBeNull();
    expect({}).not.toBeNull();
    expect('Hello World!').toEqual('Hello World!');
    expect('Hello World!').not.toEqual('Goodbye!');
    expect('Hello World!').toNotEqual('Hi!');
    expect([1, 2, 3]).toEqual([1, 2, 3]);
    expect(1).toEqual(1);
    expect({ foo: 1 }).toEqual({ foo: 1 });
    expect(1.223).toBeCloseTo(1.22);
    expect(1.233).not.toBeCloseTo(1.22);
    expect(1.23326).toBeCloseTo(1.23324, 3);
    expect([1, 2, 3]).toContain(2);
    expect([1, 2, 3]).not.toContain(4);
    expect('Hello Jasmine').toMatch(/jasmine/i);
    expect('phone: 123-45-67').toMatch(/\d{3}-\d{2}-\d{2}/);
    expect(2).toBeGreaterThan(1);
    expect(2).toBeLessThan(3);
    expect(object.doSomething).toThrow(new Error("Unexpected error!"));
  });
});

// Creating custom matchers
  beforeEach(function() {
    this.addMatchers({
      toBeGET: function() {
        var actual = this.actual.method;
        return actual === 'GET';
      },
      toHaveUrl: function(expected) {
        var actual = this.actual.url;
        this.message = function() {
          return "Expected request to have url " + expected + " but was " + actual
        };
      return actual === expected;
     }
   });
});
// Protractor API: http://angular.github.io/protractor/#/api
// Note: Most commands return promises, so you only resolve their values
 // through using jasmine expect API or using .then(function()) structure

// Control browser
browser.get('yoururl'); // Load address, can also use '#yourpage'
browser.navigate().back();
browser.navigate().forward();
browser.sleep(10000); // if your test is outrunning the browser
browser.waitForAngular(); // if your test is outrunning the browser
browser.getLocationAbsUrl() // get the current address

//Here's a trick how to wait for something to become present/visible:
browser.wait(element(by.id('some-element-id')).isPresent);

// Control buttons
element(by.id('create')).click(); // Click a button or other item

// Check visibility
element(by.id('create')).isPresent(); // Careful with this: element is often present while it's not displayed...
element(by.id('create')).isEnabled(); // enabled/disabled, as in ng-disabled...
element(by.id('create')).isDisplayed(); // Is element currently visible/displayed?

// Find an element by id, model, binding, ...
element(by.id('user_name'));
element(by.css('#myItem'));
element(by.model('person.name')); // refers to ng-model directive
element(by.binding('person.concatName')); // refers to ng-bind directive
element(by.textarea('person.extraDetails'));
element (by.input( 'username' ));
element (by.input( 'username' )).clear();
element(by.buttonText('Save'));
element(by.partialButtonText('Save'));
element(by.linkText('Save'));
element(by.partialLinkText('Save'));
element(by.css('[ng-click="cancel()"]')); 

var dog = element(by.cssContainingText('.pet', 'Dog'));
var allOptions = element.all(by.options('c for c in colors')); // when ng-options is used with selectbox

// Find collection of elements by css, repeater, xpath..
var list = element.all(by.css('.items li'));
var list2 = element.all(by.repeater('person in home.results'));
var list3 = element.all(by.xpath('//div'));
expect(list.count()).toBe(3);
expect(list.get(0).getText()).toBe('First');
expect(list.get(1).getText()).toBe('Second');
expect(list.first().getText()).toBe('First');
expect(list.last().getText()).toBe('Last');

// Send keystrokes, clear
element(by.id('user_name')). sendKeys("user1");
sendKeys(protractor.Key.ENTER);
sendKeys(protractor.Key.TAB);
element(by.id('user_name')).clear();

// Position and size, also how to deal with promises:
element(by.id('item1')).getLocation().then(function(location) {
  var x = location.x;
  var y = location.y;
});
element(by.id('item1')).getSize().then(function(size) {
 var width = size.width;
 var height = size.height;
});
// Jasmine spy / mocks
//How to spy on a method?
spyOn(obj, 'method') // assumes obj.method is a function
//How to verify it was called?
expect(obj.method).toHaveBeenCalled()
//How to verify it was called with specific arguments?
expect(obj.method).toHaveBeenCalledWith('foo', 'bar')

//How many times was it called?
obj.method.callCount

//What were the arguments to the last call?
obj.method.mostRecentCall.args

//How to reset all the calls made to the spy so far?
obj.method.reset()

//How to make a standalone spy function?
var dummy = jasmine.createSpy('dummy')
$('button#mybutton').click(dummy)

//How to have spied method also calls through to the real function?
spyOn(obj, 'method').and.callThrough()

//How do I fix the return value of a spy? spyOn(obj, 'method').and.return('Pow!')

//How to have spied method be replaced by fake implementation?
spyOn(obj, 'method').and.callFake(function() {
  return "HELLO";
});

// Arguments for callFake function are in arguments array, for example 1st argument: arguments[0]
// Prepare a mock
spyOn(configurations, 'getObjectId').and.callFake(function () {
  switch (arguments[0]) {
    case "HEADEROBJECT":
      return 1;
    case "FOOTEROBJECT":
      return 2;
    default:
      return -1;
   }
});

//How to get all arguments for all calls that have been made to the spy?
obj.method.argsForCall // this is an array

// if you want to mock without an object
var myFoo = jasmine.createSpyObj('sender', ['send']);

// Create fake implementation for empty mock
myFoo.send.andCallFake(function() {return ['some', 'fake', 'data'];});

// Same with some parameters
myFoo.send.andCallFake(function(arg1,arg2) {
  return [arg1*2,arg2*3];
});

// To expect any types
expect(this.sender.send).toHaveBeenCalledWith("my message", any(Function), any(Function));

SOAPUI, autentikointi ja Groovy

Testasin taannoin SOAPUI:lla REST rajapintoja suorituskykytestimielessä. Kun rajapinnat olivat julkisia, homma on hyvin helppoa. Mutta kun rajapinta onkin autentikoinnin takana, edessä on perinteinen testityökalujen haaste: Miten kirjautua sisään siten että seuraavat kutsut menevät samaan sessioon?

No sehän riippuu autentikoinnin tavasta. SOAP UI:sta on kirjoiteltu artikkeleita joissa käydään läpi miten siihen voi tehdä mm. BASIC/DIGEST autentikoinnilla säädöt paikalleen, ja samoin on OAUTH-standardille palikoita. Mutta yleinen ratkaisu miten siirtää tavaraa pyynnöstä toiseen oli tässä yksinkertaisin. Tempun salaisuus on tehdä test caseen erillinen askel, jossa varastoidaan autentikoinnin tieto kontekstiin, josta sen voi myöhemmin pukata haluamaansa paikkaan, esim. http headeriin tms.

Eli sanotaan että askel 1 on määritetty olemaan REST kutsu nimellä ’login’, joka lähettää tunnusta ja salasanaa, ja vastaanottaa tokenin, esimerkissä nimellä ’identityToken’. Seuraava askel on avata SOAP UI:ssa test case, ja lisätä sinne uusi askel.(Add Step – Groovy Script).

Tänne kirjoitellaan koodi joka imaisee tokenin vastauksesta, esim. tähän tapaan:

// This script will grab authentication token and store it in testcase properties so it can be used later

xmlResponse = new XmlSlurper().parseText(context.expand( ’${login#ResponseAsXml}’ ))

assert xmlResponse.sessionId != null
assert xmlResponse.sessionId.toString().length() > 0
log.info(’Got ’ + xmlResponse.sessionId)
testRunner.testCase.setPropertyValue( ”authToken”, xmlResponse.sessionId.toString())

Eli pari huomiota tästä esimerkistä:

  • Tässä hyödynnetään SOAPUI:n ominaisuutta auto-konvertoida json xml muotoon – eli vastaus voi olla xml tai jsonia, kaikki käy
  • context.expand viittaa login-stepin sisältöön xml:ksi rutistettuna
  • Pseudo-xml kenttiin viitataan xmlResponse.xxx tapaan kuten yllä
  • log.info on tässä vain esimerkin vuoksi, sillä saa tulostusta
  • testcasen propertyyn lisätään authToken niminen arvo jossa on sessionid varastoituna

Tätä voi sitten vuorostaan käyttää missä haluaakin, esim. header kentässä tai asserteissa, tähän tapaan:

${#TestCase#authToken}

SOAPUI ja LOADUI ovat ihan metkoja kapistuksia joilla aika helposti pyöräyttää vähän integraatio/api/suorituskykytestejä.

Angular Protractor ja Drag&Drop testaus

No niin, mitäpä ei moderni tablettikäyttöliittymä olisi ilman drag&drop kikkailua. Ajankohtaiseksi tuli miettiä miten sellaista voisi testata, automatisoidusti.

Onnekkaasti protractorista löytyi tähän suoraan rahkeet, jotka jopa toimivat:

// Grab two panels
var panels = element.all(by.repeater(’panel in panelsInGrid’));
var panel1 = panels.get(0);
var panel2 = panels.get(1);

// Drag&drop to new location
browser.actions().dragAndDrop(
panel1,
panel3
).perform();

Ja sillä siisti. Toimii (paremmin kuin) junan vessa. Joskus on tarve absoluuttisille koordinaateille:

browser.actions().dragAndDrop(
panel1DragHandleEast,
{x: 200, y: 200}
).perform();

Jep, sekin onnistuu. Onkohan jotain mitä Protractorilla ei voi automatisoida.. 😉

Google Chrome ja Protractor timeout mysteeri ratkennut

Kirjailin taannoin tänne testiautomaatiota riivanneesta pulmasta, kun yllättäen Chrome selaimella ajettavat Protractor testit alkoivat epäonnistua timeout-virheiden vuoksi. Syyksi paljastui Chrome version 38 bugi suhteessa Selenium Webdriveriin. Tilapäinen korjaus jonka taisin tänne myös kirjoitella oli poistaa koneesta Chrome 38, asentaa tilalle Chrome 37, ja sitten disabloida auto-update.

Bugi johon tässä viittaan löytyy täältä: https://code.google.com/p/chromium/issues/detail?id=422218

Chrome 39 with updates disabled

Nyt on tuoreeltaan julkaistu uusi Chrome versio 39, jossa tuo bugi on korjattu, ja sain juuri verifioitua että homma toimii taas silläkin ihan aikuisten oikeesti. Eli protractorin kanssa puljatessa homma menee nyt näin: Chrome 39 asennettuna, ja päivitykset edelleen disabloituna. Tästedes päivitetään manuaalisesti.

Tuossa on linkki paikkaan jossa muun muassa käydään läpi tuota auto-päivitysten disablointia:

http://watirmelon.com/2014/11/05/lock-down-your-browser-versions-if-you-run-webdriver-tests/

Huomattakoon että halutessaan voi myös editoida rekisteriä käsin.

Tuossa on Chrome 39 changelog: http://googlechromereleases.blogspot.fi/2014/11/stable-channel-update_18.html

Chrome 39 on muuten myös nopeampi, ja sisältää muitakin bugikorjauksia. Saatavana myös Androidille! 😉

Protractor, Firefox ja InvalidElementStateError

No niin, aina oppii jotain uutta ja ihmeellistä. Protractor e2e testiautomaation pyörintä on vähän viime viikkoina yskinyt. Ensimmäinen ongelma oli automaatioserverin timeoutit, joiden syyksi paljastui jonkunmoinen bugi chrome selaimessa/sen driverissa.

Korjaukseksi otettiin käyttöön Firefox odotellessa Chromen palautumista toimintakuntoiseksi. Firefox ajoi iloisesti kaikki muut testit paitsi ne joissa valitaan selection listasta joku tietty arvo sisällön perusteella. Originaali koodi:

//Open selection box
selectBoxElement.click();
browser.waitForAngular();
selectBoxElement.element(by.cssContainingText('option', item)).click();
browser.waitForAngular();

SelectBoxElement on tässä se laatikko josta valinta tehdään, ja item on merkkijono jonka sisältö tulisi löytyä valittavasta arvosta. Toimii hienosti Chromessa! (Tai toimi… Grrr…)

No Firefox ei tästä tykkää. Avaa kyllä boxin ja valintakin siirtyy oikeaan kohtaan kyllä, mutta jotenkin silti boxin suljettua vanha arvo edelleen löytyy. Syyksi paljastui bugi Selenium webdriverissa, joka ai lauo change eventtiä oikein tässä tilanteessa Firefoxissa.

No eipä hätää, Interwebin voimalla löytyi korjaus tähän:

//Open selection box
 selectBoxElement.click();
 browser.waitForAngular();

// Make selection
selectBoxElement.element(by.cssContainingText('option', item)).click();
browser.waitForAngular();

// Firefox driver fix to trigger change event for Angular
browser.actions().mouseDown().mouseUp().perform();
browser.waitForAngular();

Joka toimikin viikon. Ja nyt:

InvalidElementStateError: [Exception… ”Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsINativeMouse.click]”

Jep jep, ja tähän ei löydy patchejä oikein mistään. Yhtäkkiä siis joko webdriver tai firefox saa halvauksen tuosta simuloidusta klikkauksesta, mahdollisesti koska se on väärässä kohtaa ruutua?

Tätä saikin sitten aikansa ihmetellä. Miten syvään suohon sitä voi fanaattisella testiautomaatiolla päätyä.. Kokeilin kaikenlaista, lähettää erinäisiä loitsuja sendKeys metodilla select laatikolle, arvolle, käyttää promiseja asynkroniseen etenemiseen siltä varalta että tämä on joku ajastusongelma. Ei apua, ei toimi.

Vaan toimiipa sittenkin. Sometimes less is more. Käy ilmi että valinnan voi tehdä myös epäintuitiivisesti avaamatta selectoboxia ennen. Käy ilmi että tämä toimii Chromessa ja Firefoxissa. Käy ilmi että nykyisellään tämä on ainoa tapa tehdä valinta selectboxista onnistuneesti.

Eli tuunauksen jälkeen, tässä oman autotallin algoritmi selectboxin valintaan protractorissa:

module.exports.selectDropdownByText = function selectOption(selectBoxElement, item) {
   selectBoxElement.element(by.cssContainingText('option', item)).click();
   browser.waitForAngular();
};

Ja taas toimii.

Grunt, Protractor, ja Istanbul kattavuusraportit e2e-testeille

Tuli ajankohtaiseksi pohtia voiko API ja E2E testeistä saada koodikattavuusanalyysejä. Käy ilmi, että Angular-alustalla Protractor ajoista voi. Yritin ensin helppoa reittiä valmiin grunt-protractor-coverage plug-inin kanssa, mutta törmäsin toistuvasti kahteen ärsyttävään virheeseen joista ei löytynyt lisätietoa ja jotka eivät ratkeeneet. Joten löysin ratkaisun joka on vielä yksinkertaisemmista paloista koottu: Instanbul moduuli ja vähän magiaa.

Istanbulille on tehty monia Grunt-plugineita, itse päädyin käyttämään perusmallia taichi.

https://github.com/taichi/grunt-istanbul

Plugarin asennus ja lataus on ihan normikamaa. Tärkeät taskimäärittelyt ovat instrument, ja makeReport.

Instrument tähän tapaan:

    instrument: {
      files: 'app/*.js',
      options: {
        lazy: true,
        basePath: 'test/coverage/instrument/'
      }
    }

Tuossa app/*.js on kansio jossa koodit muhivat, ja test/coverage/instrument on kansio johon instrumentoidut versiot koodista kopioituvat. Tämä prosessi muuttaa koodien rakennetta rajusti joten todella syytä pitää erillisiä kopioita, ja suojata omat lähdekoodit.

Tämän magian kannalta on hyvä lisätä vielä coverageVariable muuttuja, eli lopullinen ratkaisu tässä:

    instrument: {
      files: 'app/*.js',
      options: {
        lazy: true,
        coverageVariable: '__coverage__',
        basePath: 'test/coverage/instrument/'
      }
    }

Näin kun tätä instrumentoitua koodia ajetaan, kutsut päivitetään tähän globaaliin coverage muuttujaan – jonka me voimme napata (oletusnimi on aika härpäke aikaleimoineen, siksi määrittelemme sen uudelleen).

Tämän ohella on syytä clean-taskilla putsata ensin coverage kansio, sitten copy-taskilla siirtää kaikki ei-javascript tauhkat joita e2e testaukseen tarvitaan, sisältäen esim. html, kuvatiedostot, tyylisivut, jne – tietysti valmiiksi prosessoituna e2e-käyttöön kelvollisina. Tässä esimerkki copy-taskin sisällöstä:

coverageE2E: {
 files: [
 {expand: true, cwd: './dist/lib', src: '**/*.*', dest: 'coverageE2E/lib'},
 {expand: true, cwd: './dist/', src: '**/*.html', dest: 'coverageE2E'},
 {expand: true, cwd: './dist', src: '**/*.png', dest: 'coverageE2E'},
 {expand: true, cwd: './dist', src: '**/*.jpg', dest: 'coverageE2E'},
 {expand: true, cwd: './dist', src: '**/*.gif', dest: 'coverageE2E'},
 {expand: true, cwd: './dist', src: '**/*.ico', dest: 'coverageE2E'},
 {expand: true, cwd: './dist', src: '**/*.svg', dest: 'coverageE2E'},
 {expand: true, cwd: './dist', src: '**/*.css', dest: 'coverageE2E'},
 {expand: true, cwd: './dist', src: '**/*.css', dest: 'coverageE2E'},
 {expand: true, cwd: './dist/fonts', src: '*.*', dest: 'coverageE2E/fonts'},
 {expand: true, cwd: './dist/lib', src: '*.*', dest: 'coverageE2E/lib'},
 {expand: true, cwd: './WEB-INF', src: '*.*', dest: 'coverageE2E/WEB-INF'},
 // Here, extra copy operation since istanbul puts instrumented code in a subdir to a wrong path
 {expand: true, cwd: './coverageE2E/dist', src: '**/*.*', dest: './coverageE2E'}
 ]
 }

Nyt kun asiaa oikein ääneen mietin, niin kenties tätäkin voisi yleistää, esim. kopioi kaikki ei- .js päätteiset tiedostot?

Nykyisellään Istanbul instrumentointi pakottaa kohdekansioon hakemistorakenteen missä lähdekansio on – joka ei ole aina haluttua. En löytänyt pluginista säätöjä, joten tuossa copy taskissa kopioidaan dist-kansion sisältö tasoa ylemmäs, jotta se on rakenteessa jota itse käytän myös testeihin ja tuotantoon.

Seuraavaksi tulee se dirty trick: Napataan kerätty __coverage__ muuttuja ja kirjoitellaan se tiedostoon, jahka kaikki coverage on kerätty. Tämä on aivan varmasti tehtävissä nätimminkin – mutta tässä on esimerkkinä protractor-taskin onComplete-funktio joka ajetaan sen lopuksi:

onComplete: function() {
  // Let's write code coverage analysis to a file!
  browser.driver.executeScript("return __coverage__;").then(function(val) {
  fs.writeFileSync("coverageE2E/coverage.json", JSON.stringify(val));
  });
}

Lopuksi analysoidaan coverage.json tiedosto makeReport-taskilla:

module.exports = {
 src: 'coverageE2E/coverage.json',
 options: {
 type: 'lcov',
 dir: '../../../target/e2ecoverage',
 print: 'detail'
 }
};

Ja tässä kooste koko e2e_coverage taskista:

 

 

module.exports = function(grunt) {
 grunt.registerTask('e2e_coverage', [
   'dev', // First do normal build to /dist folder
   'clean:coverageE2E', // clean away old instrumented code
   'instrument', // Instrument all javascript files to instrumented folder using Istanbul
   'copy:coverageE2E', // Copy all non-javascript resources from dist to instrumented code
   'express:coverageE2E', // Run express server using instrumented folder, not dist folder
   'protractor:coverage', // Run protractor tests using instrumented folder
   'makeReport' // Use Istanbul reporting
  ]);
};

Eli tiiviisti: Käännä, putsaa vanhat pois, instrumentoi koodi Istanbulilla, kopioi html:t, tyylisivut ja kuvat sinne sekaan, aja serveri ja protractor testit instrumentoidun koodin kansiosta, ja rakenna raportti tuotoksista, taas Istanbul-pluginilla.

Nyt – tätä olisi mukavaa analysoida Sonarissa, mutta näyttäisi että siellä on Javascript-pluginissa paikka vain yhdelle lcov tiedostolle…

Huom. myös – tuon coverage tiedon keruuvaiheen voisi tehdä elegantimminkin grunt-protractor-coverage pluginilla, löytyy täältä:

https://www.npmjs.org/package/grunt-protractor-coverage

Omissa kokeiluissa en kuitenkaan saanut tuota toimimaan, se antoi kahta eri virhettä – voi johtua käytössä olevista kirjastokombinaatioista, kansiorakenteista, tms, mutta en viitsinyt haaskata siihen enempää aikaa.