Testcafe & Cucumber

Et si nos POs commençaient à faire leur travail?

+

+

Il était une fois...

Il était une fois...

  • Un développeur consciencieux & son application web Angular.
  • Avec une couverture de tests unitaires presque parfaite.

Il était une fois...

  • Mais un jour, l'impossible se produit: un BUG en production...
  • Et des chefs de projet en colère!

Il était une fois...

A chaque push? Nightly build? Release?

Sur 1-n navigateurs? Via un Browserstack-like?

Comment les écrire? Les lancer?

Et le reporting?

QUI

DOIT

LES

ECRIRE???

Que faire pour ne pas réitérer ce problème? 

Des tests end to end!

Pour lancer de manière automatisée des scénarios de tests!

Ce qui nous amène à...

Sommaire

1)  e2e: existant et problématiques

2) Lancer ses tests avec Testcafe

3) Écrire ses tests avec cucumber-js

4) Solutions retenues

5) Conclusion & Questions

Etat de l'art...

1) e2e: existant et problématiques

En Angular, le framework de tests e2e par défaut est Protractor.

  • Un programme Node.js utilisant Selenium
  • Supporte les frameworks de tests Jasmine et Mocha.

1) e2e: existant et problématiques

Comment fonctionnent Protractor et Selenium?

1) e2e: existant et problématiques

C'est un peu lourd à installer tout ça, non?

webdriver-manager update --proxy https://proxy.com:8080 
 ✹ ✭ webdriver-manager: using local installed version 12.0.6 events.js:160
      throw er; // Unhandled 'error' event
I/hosted - Using the selenium server at http://localhost:4444/wd/hub
I/launcher - Running 1 instances of WebDriver
E/launcher - Error code: 135
E/launcher - Error message: ECONNREFUSED connect ECONNREFUSED 127.0.0.1:4444
[14:19:22] I/launcher - Running 1 instances of WebDriver
[14:19:22] I/hosted - Using the selenium server at http://localhost:4444/wd/hub
[14:19:22] E/launcher - The driver executable does not exist: /root/.npm-global/lib/node_modules/protractor/node_modules/webdriver-manager/selenium/chromedriver_2.31

1) e2e: existant et problématiques

Et l'écriture des tests avec tout ça?

describe('my todo list', function() {
  it('should add a todo', function() {
    browser.get('https://my-todo-list.org');

    element(by.model('todoList.todoText'))
      .sendKeys('write first protractor test');
    element(by.css('[value="add"]')).click();

    var todoList = element.all(by.repeater('todo in todoList.todos'));
    expect(todoList.count()).toEqual(3);
    expect(todoList.get(2).getText())
      .toEqual('write first protractor test');

    todoList.get(2).element(by.css('input')).click();
    var completedAmount = element.all(by.css('.done-true'));
    expect(completedAmount.count()).toEqual(2);
  });
});

Whhhhhhaaaaaat????

1) e2e: existant et problématiques

PO: "Euh, ça veut dire quoi ce test là? C'est censé représenter quel cas métier?"

Dev remplaçant: "Bah je sais pas trop, je creuse (ma tombe)"

1) e2e: existant et problématiques

Les gars, j'ai ZE IDEA!!!

DEV

BADASS

1) e2e: existant et problématiques

Et si on utilisait le pattern Page Object?

'use strict';

var AngularPage = function () {
  browser.get('http://my-todo-list.org');
};

 AngularPage.prototype = Object.create({}, {
    todoText: { get: function () { return element(by.model('todoText')); }},
    addButton: { get: function () { return element(by.css('[value="add"]')); }},
    yourName: { get: function () { return element(by.model('yourName')); }},
    greeting: { get: function () { return element(by.binding('yourName')).getText(); }},
    todoList: { get: function () { return element.all(by.repeater('todo in todos')); }},
    typeName: { value: function (keys) { return this.yourName.sendKeys(keys); }},
    todoAt: { value: function (idx) { return this.todoList.get(idx).getText(); }},
    addTodo: { value: function (todo) {
    this.todoText.sendKeys(todo);
    this.addButton.click();
  }}
});

module.exports = AngularPage;

1) e2e: existant et problématiques

Grâce à mon PageObject, les tests sont compréhensibles!

Sauf que...

1) e2e: existant et problématiques

Grâce à mon PageObject, les tests sont compréhensibles!

J'ai fait pareil...

2

J'ai fait pareil...

3

etc...

TestCafe à la rescousse!

2) Lancer ses tests avec TestCafé

Qu'est-ce que c'est?

  • Outil Node.js d'automatisation de tests e2e
  • Gratuit & Open Source
  • Ecriture des tests en JS (ES2017) ou Typescript
  • Installé en une minute!
  • Pas besoin d'outil tiers comme Webdriver
npm install -g testcafe
testcafe chrome mon-test.js

2) Lancer ses tests avec TestCafé

Les (autres) avantages:

  • Timeouts manuels
  • testcafe-live
  • Concurrency
  • Remote testing
  • ...

2) Lancer ses tests avec TestCafé

Comment ça fonctionne?

testcafe-hammerhead

2) Lancer ses tests avec TestCafé

Notre premier test:

import { Selector } from 'testcafe';

fixture `Hello world classique`
    .page `https://devexpress.github.io/testcafe/example`;


test('Saluer le développeur', async t => {
    await t
        .typeText('#developer-name', 'Laurent Wroblewski')
        .click('#submit-button')
        .expect(Selector('#article-header').innerText)
          .eql('Hello Laurent Wroblewski!');
});

Page Object pattern!!!

2) Lancer ses tests avec TestCafé

Utilisation des Selectors TestCafe:

import { Selector } from 'testcafe';

fixture `Example page`
    .page `http://devexpress.github.io/testcafe/example/`;

test('My test', async t => {
    const osCount            = Selector('.column.col-2 label').count;
    const submitButtonExists = Selector('#submit-button').exists;

    const secondCheckBox = Selector('input')
        .withAttribute('type', 'checkbox')
        .nth(1);

    await t
        .expect(osCount).eql(3)
        .click(secondCheckBox)
        .expect(submitButtonExists).ok();
});

2) Lancer ses tests avec TestCafé

Utilisation des Selectors TestCafe:

  • Représente un sélecteur vers X éléments du DOM.
  • Permet d’exécuter des actions sur ces éléments.
  • Utilisables dans les assertions.
  • Filtrer les éléments (par texte, attribut).
import { Selector } from 'testcafe';

const productCard = Selector('.product-card');
await t.click(productCard);
await t.expect(productCard.scrollHeight)
  .eql(300);
const selectedProductCard =
    Selector('.product-card')
        .withAttribute('selected');

2) Lancer ses tests avec TestCafé

Création de Selectors custom:

import { Selector } from 'testcafe';

const elementWithIdOrClassName = Selector(value => {
    return document.getElementById(value) || document.getElementsByClassName(value);
});

2) Lancer ses tests avec TestCafé

Extension de Selectors:

interface UserTableSelector extends Selector {
    getCellContent(rowIndex: number, columnIndex: number): Promise<string>;
}

const userTable: UserTableSelector = <UserTableSelector>Selector('#users')
    .addCustomMethods({
        getCellText: (table: HTMLTableElement, rowIndex: number, columnIndex: number) => {
            return table.rows[rowIndex].cells[columnIndex].innerText;
        }
    }
);

await t.expect(userTable.getNameCellContent(1)).contains('Dupond');

2) Lancer ses tests avec TestCafé

SELECTORS SEAL OF APPROVAL

2) Lancer ses tests avec TestCafé

Librairies de sélecteurs pour différents frameworks :

testcafe-angular-selectors

testcafe-react-selectors

testcafe-vue-selectors

testcafe-aurelia-selectors

import { AngularSelector } from 'testcafe-angular-selectors';

const list        = AngularSelector('list');
const listAngular = await list.getAngular();

await t.expect(listAngular.inputState).eql(1);

2) Lancer ses tests avec TestCafé

Des providers existent pour l'intégration avec:

  • BrowserStack
  • Sauce Labs
  • electron
npm install testcafe-browser-provider-browserstack

npm install testcafe-browser-provider-saucelabs

npm install testcafe-browser-provider-electron

2) Lancer ses tests avec TestCafé

En conclusion :

  • Facile à installer et à lancer
  • APIs intuitives et faciles à customiser
  • S'intègre bien à vos process d'intégration continue

Question: Qu'en est-il de la concurrence?

Navigateurs supportés: Chrome & co

C'est bien beau tout ça...

Mais seuls les développeurs peuvent écrire ce genre de tests?!

Cucumber à la rescousse!

3) Ecrire ses tests avec cucumber-js

  • Famille des Cucurbitacées
  • Implémentation JS de cucumber
  • Outil de lancement de tests automatisés BDD 
  • Ecriture des tests en Gherkin

GIVEN un contexte initial

WHEN une action est effectuée

THEN un comportement est attendu

3) Ecrire ses tests avec cucumber-js

  • Comment l'utiliser?
$ npm install cucumber
$ ./node_modules/.bin/cucumber-js features/**/*.feature

3) Ecrire ses tests avec cucumber-js

  • Les fichiers feature décrivent les tests métiers.
  • Chaque scénario représente un cas de test indépendant.
# features/user_login.feature
Feature: Login utilisateur
  En arrivant sur le site, l'utilisateur se connecte pour accéder à son profil.

  Scenario: Affichage de la Popup de login
    Given Je me rends sur la page home
    When Je clique sur le bouton "Connexion"
    Then La modale "Connexion au site" devrait apparaitre

  Scenario Outline: Connexion utilisateur
    Given Je me rends sur la page home
    When Je clique sur le bouton "Connexion"
    And Je saisis la valeur "<login>" dans le champ de saisie "Votre login"
    And Je saisis la valeur "<password>" dans le champ de saisie "Votre mot de passe"
    And Je clique sur le bouton primaire
    Then Le message suivant devrait apparaitre: "<result_login>"

    Examples:
      | login | password | result_login |
      | Tom   | 1234     | Erreur       |
      | Valid | Titi456! | Succès       |

3) Ecrire ses tests avec cucumber-js

  • Step Definitions: glue entre les features et le système de tests.
import { Before, Given, Then, When } from 'cucumber';
import {PageObject} from '../page-objects/page-object';

When(/^Je me rends sur la page home$/, async function() {
  await this.page.gotoPage('');
});

When(/^Je clique sur le bouton "(.*?)"/, async function(buttonLabel: string) {
  await this.page.clickOnButton(buttonLabel);
});

When(/^Je saisis la valeur "(.*?)" dans le champ de saisie "(.*?)"$/,
    async function(value, inputLabel) {
  await this.page.writeInInput(inputLabel, value);
});

When(/^Je clique sur le bouton primaire$/, async function() {
  await this.page.clickOnPrimaryButton();
});

Euh... Juste un petit problème... ?

Donc on fait installer NodeJS, Webstorm/VSCode, etc à nos POs pour qu'ils puissent écrire leurs tests...?

2 besoins:

Rendre le développeur heureux

Rendre le PO heureux

4) Mélanger testcafe & cucumber

  • L'écriture de tests en Gherkin avec cucumber-js
  • La facilité de développement et de setup de TestCafe
  • Comment faire?
  • Grâce à la mécanique de World / Hook de cucumber

4) Mélanger testcafe & cucumber

  • Qu'est-ce qu'un Hook cucumber?
  • Un ensemble de points d'entrée pour initialiser et détruire l'environnement de test, avant et après chaque scénario
var {After, Before} = require('cucumber');

Before(function () {
  ...
});

Before(function (testCase, callback) {
  ...
  callback();
});

After(function () {
  ...
});

4) Mélanger testcafe & cucumber

Et si on lançait une instance TestCafe via un Hook cucumber?

4) Mélanger testcafe & cucumber

  • L'API TestCafe fournit la méthode createTestCafe, qui permet d’exécuter une instance testcafe:
createTestCafe('localhost', 1338)
    .then(testcafe => {
        return testcafe.createRunner()
            .src('test.js')
            .browsers('chrome')
            .run();
    });

4) Mélanger testcafe & cucumber

  • Before du Hook cucumber:
Before(function() {
  createTestCafe(config.host, 1338, 1339)
    .then(tc => {
      testcafe = tc;
      const runner = testcafe.createRunner();
      return runner
        .useProxy(config.proxy)
        .src(`node_modules/bdd-launcher/lib/test.js`)
        .screenshots(config.screenshotsPath, false)
        .concurrency(config.concurrency)
        .browsers(config.browsers)
        .run({ debugMode: config.debug })
        .then(() => {
          testcafe.close();
          runner.stop();
        })
        .catch(() => {
          console.log('closing testcafe on error...');
          testcafe.close();
        });
    });
  return this.waitForTestController
    .then(testController => testController.maximizeWindow());
});

4) Mélanger testcafe & cucumber

  • Définition des steps avec les APIs TestCafé:
import { ClientFunction, Selector } from 'testcafe';
import { waitForAngular, AngularSelector } from 'testcafe-angular-selectors';
import {TestCafeWorld} from '../support/world';

export class PageObject {

  getLocation = ClientFunction(() => document.location.href);
  goBack = ClientFunction(() => window.history.back());

  constructor(private t: TestController, private world: TestCafeWorld) {}

  getContent(): Selector {
    return this.getElement('body');
  }

  buttons(): Selector {
    return this.getElement('button');
  }

  buttonWithLabel(label: string): Selector {
    return this.buttons().withText(label);
  }

  gotoPage(page: string): Promise<any> {
    return this.t.navigateTo('/' + page);
  }

4) Mélanger testcafe & cucumber

  • Et pourquoi tu nous dis tout ça?
$ npm i bdd-testcafe --save-dev
$ npm run e2e
$ npm run e2e:report

Je suis très content!

Et pour les POs?

4) TestCafé Studio?

4) Solution maison: bdd-editor

En conclusion...

Conclusion...

  • TestCafe: c'est bien
  • cucumber-js: c'est bien aussi
  • Les deux c'est mieux!
  • Mais ça ne suffit pas...

Et plus sérieusement?

Conclusion...

  • Les tests e2e sont TRÈS importants...
  • ... Mais pas tels qu'ils sont implémentés dans 90% des cas.

Les tests e2e sont:

  • Réservés aux seuls développeurs.
  • Une vérification du fonctionnement technique de mon code.
  • Accessibles à tous les membres de l'équipe
  • Une documentation métier, et source de validation de specs métier

Merci à tous!!!

Des questions?

testcafe & cucumber: Et si nos POs travaillaient un peu?

By Laurent WROBLEWSKI

testcafe & cucumber: Et si nos POs travaillaient un peu?

  • 667