BDD toepassen in React met jest-cucumber
*Gherkin*, *Cucumber* en *BDD*. Allemaal termen die gloednieuw zijn voor mij. Daarom heb ik ervoor gekozen om dit te onderzoeken in een React omgeving. React is mijn favoriete JavaScript framework en testen is nooit mijn passie geweest. Door het toevoegen van BDD in React gaat hier misschien wel verandering in komen.
Jest-Cucumber
jest-cucumber is an alternative to Cucumber.js that runs on top on Jest. Instead of using describe and it blocks, you instead write a Jest test for each scenario, and then define Given, When, and Then step definitions inside of your Jest tests. jest-cucumber then allows you to link these Jest tests to your feature files and ensure that they always stay in sync (Npm: Jest-Cucumber, 2021).
In principe is jest-cucumber dus een combinatie van Jest en Cucumber.
Jest is een open-source JavaScript Test runner ontwikkelt door Facebook (facebook, z.d.). Web Development studenten zijn hier al mee bekend aangezien de DWA course dit uitgebreid behandelt.
Jest is an excellent test runner with great features like parallel test execution, mocking, snapshots, code coverage, etc. If you're using VS Code, there's also a terrific Jest extension that allows you get realtime feedback as you're writing your tests and easily debug failing tests individually. Cucumber is a popular tool for doing Acceptance Test-Driven Development and creating business-readable executable specifications. This library aims to achieve the best of both worlds, and even run your unit tests and acceptance tests in the same test runner. (Npm: Jest-Cucumber, 2021)
Reguliere JavaScript
Om simpel te beginnen maak ik een rekenmachine applicatie. Puur om te bekijken hoe jest-cucumber te werk gaat en de code zo simpel mogelijk te houden. Hiervoor maken we een aantal bestanden:
De inhoud van de bestanden ziet er als volgt uit:
Calculator.feature
: De feature file, hier bevindt zich alle business acceptatiecriteria in Gherkin notatie.Calculator.js
: De daadwerkelijke klasse die we gaan testen.Calculator.test.js
Hier bevindt zich alle glue code om de Gherkin code te vertalen naar JavaScript.
We beginnen met een simpel scenario om de glue code in elkaar te bouwen:
# Calculator.feature
Feature: Calculator
Scenario Outline: Adding Numbers
Given I have a Calculator
When I add <Number1> and <Number2>
Then I should get <Result>
Examples:
| Number1 | Number2 | Result |
| 1 | 2 | 3 |
| 0 | 0 | 0 |
| 99 | 99 | 198 |
| -4 | 5 | 1 |
| -4 | -4 | -8 |
Hierboven staat de werking van een rekenmachine die kan optellen genoteerd. Ik gebruik een Scenario Outline
, zodat ik meerdere scenario's tegelijk kan testen via het opgeven van een Examples
. Jest-cucumber ondersteunt dit. Om ervoor te zorgen dat er glue code gemaakt kan worden dienen we zelf code te schrijven. Namelijk een testbestand die het feature bestand inlaadt:
// Calculator.test.js
import { defineFeature, loadFeature } from "jest-cucumber";
const feature = loadFeature("./Calculator.feature");
defineFeature(feature, (test) => {});
Hierboven wordt het feature bestand ingeladen met behulp van de loadFeature
functie uit jest-cucumber. Vervolgens passen wij defineFeature
toe. Hierbinnen wordt de daadwerkelijke glue code geschreven. Als ik de test nu probeer uit te voeren verschijnt er in de console een melding dat er geen step definitions gevonden konden worden. Vervolgens wordt er een lijst gegeven met de mogelijke step definities die je kan gebruiken. Kwestie van kopiëren en plakken dus.
Nu is het een kwestie van invullen.
defineFeature(feature, (test) => {
let calculator;
let result;
const givenIHaveACalculator = (given) => {
given("I have a Calculator", () => {
calculator = new Calculator();
});
};
const thenIShouldGetNumber = (then) => {
then(/^I should get (.*)$/, (arg0) => {
expect(result).toBe(parseInt(arg0));
});
};
test("Adding numbers", ({ given, when, then }) => {
givenIHaveACalculator(given);
when(/^I add (.*) and (.*)$/, (arg0, arg1) => {
result = calculator.add(parseInt(arg0), parseInt(arg1));
});
thenIShouldGetNumber(then);
});
});
Jest-cucumber biedt ook de mogelijkheid aan om step-definities meerdere keren te gebruiken, dit is nu het geval bij givenIHaveACalculator
en thenIShouldGetNumber
. Zo kan ik bijvoorbeeld een extra scenario toevoegen voor vermenigvuldigen, aftrekken of delen. Dan moet ik enkel de when step opnieuw schrijven.
Jest-cucumber ondersteunt een lijst met andere functionaliteiten zoals asynchroon, gherkin tables of automatic step binding. De documentatie is hier te vinden. Alle code hierboven beschreven is uitgewerkt en is hier terug te kijken.
React Componenten
Hoe zit het nu precies met React? Je zou misschien denken dat het opeens heel ingewikkeld gaat worden omdat je nu een browser nodig hebt. Dat is niet zo! React biedt de mogelijkheid aan om componenten te testen zonder het gebruik van een browser of afhankelijk te zijn van een DOM. Hierbij kan je gebruik maken van react-test-renderer
.
This package provides a React renderer that can be used to render React components to pure JavaScript objects, without depending on the DOM or a native mobile environment.
Essentially, this package makes it easy to grab a snapshot of the platform view hierarchy (similar to a DOM tree) rendered by a React DOM or React Native component without using a browser or jsdom.
(Test Renderer –, z.d.)
Cool! We kunnen dus React Componenten testen zonder gebruik te maken van een browser! Hoe react-test-renderer
precies te werk gaat valt buiten de scope van het project, mocht je hier meer over weten is dit hier te bekijken.
Om consistent te blijven heb ik een rekenmachine gebouwd in een React component.
// Calculator.jsx
<div>
<input type="number" id="number1" value={number1} onChange={setNumber1}></input>
<select id="option" value={option} onChange={setOption}>
<option value="increment">+</option>
<option value="substract">-</option>
<option value="divide">/</option>
<option value="times">*</option>
</select>
<input type="number" id="number2" value={number2} onChange={setNumber2}></input>
<span>
= <span id="result">{result}</span>
</span>
<button id="startCalculation" onClick={handleCalculation}>
Calculate
</button>
</div>
In bovenstaande code is het JSX element zichtbaar. Overige code, de daadwerkelijke functionaliteit, raken wij met testen niet direct aan. Met het testen gaan wij input velden aanpassen, op knopjes drukken en bekijken wat het eindresultaat is. De volledige code is hier zichtbaar.
# Calculator.feature
Feature: Using the Calculator
Using the calculator within the React App
Scenario Outline: Adding Numbers
Given I open the Calculator App
When I enter numbers <number1> and <number2>
And I click the addition option
And I click the calculate button
Then I should get <result>
Examples:
| number1 | number2 | result |
| 1 | 2 | 3 |
| 0 | 0 | 0 |
| 5 | -4 | 1 |
| 99 | 99 | 198 |
| -5 | -5 | -10 |
Nu wij een leesbare feature file hebben is het tijd om glue code te schrijven. Zoals eerder beschreven worden je step definities weer gegenereerd.
import { defineFeature, loadFeature } from "jest-cucumber";
import { create, act } from "react-test-renderer";
import Calculator from "./Calculator";
const feature = loadFeature("./src/Calculator.feature");
defineFeature(feature, (test) => {
let testInstance;
test("Adding Numbers", ({ given, when, and, then }) => {
given("I open the Calculator App", () => {
const testRenderer = create(<Calculator />);
testInstance = testRenderer.root;
});
when(/^I enter numbers (.*) and (.*)$/, (arg0, arg1) => {
const inputN1 = testInstance.findByProps({ id: "number1" });
const inputN2 = testInstance.findByProps({ id: "number2" });
act(() => {
inputN1.props.onChange({ target: { value: arg0 } });
inputN2.props.onChange({ target: { value: arg1 } });
});
});
and("I click the addition option", () => {
const inputOption = testInstance.findByProps({ id: "option" });
act(() => {
inputOption.props.onChange({ target: { value: "increment" } });
});
});
and("I click the calculate button", () => {
const buttonCalculate = testInstance.findByProps({ id: "startCalculation" });
act(() => {
buttonCalculate.props.onClick();
});
});
then(/^I should get (.*)$/, (arg0) => {
const spanResult = testInstance.findByProps({ id: "result" });
expect(spanResult.props.children).toBe(parseFloat(arg0));
});
});
});
Hierboven is de daadwerkelijke test genoteerd. De kern is vrijwel hetzelfde, je ziet namelijk nog steeds given
, when
& then
terugkomen. Het grootste verschil is de daadwerkelijke test code. In plaats van direct de klasse aanroepen die wij testen maken wij gebruik van een react-test-renderer. Door hier gebruik van te maken ontstaat de mogelijkheid om gebruikerinteracties te simuleren. De gebruikerinteracties staan altijd in een act()
functie.
Neem als voorbeeld:
and("I click the calculate button", () => {
const buttonCalculate = testInstance.findByProps({ id: "startCalculation" });
act(() => {
buttonCalculate.props.onClick();
});
});
Ten eerste wordt het button element opgehaald aan de hand van een id, in dit geval startCalculation
. Vervolgens wordt erop geklikt binnen een act()
functie. Nu wordt op de achtergrond daadwerkelijke berekeningen uitgevoerd en wordt dit aangepast op de DOM. Hé maar eerder zei ik dat er geen DOM was. Klopt! In plaats van het aanpassen van de DOM wordt de testInstance aangepast. TestInstance is de vervanging van de DOM als een JavaScript object (Test Renderer –, z.d.).
then(/^I should get (.*)$/, (arg0) => {
const spanResult = testInstance.findByProps({ id: "result" });
expect(spanResult.props.children).toBe(parseFloat(arg0));
});
Hierboven wordt het resultaat uit precies dezelfde testInstance gehaald. Alle gebruikerinteracties die worden uitgevoerd zijn direct zichtbaar in testInstance!
Uiteraard zijn alle functionaliteiten van jest-cucumber, zoals het hergebruiken van step-definitions, asynchroon en Gherkin Tables ook gewoon beschikbaar! In bovenstaande afbeelding is te zien dat niet alle branches zijn getest. Dit komt doordat niet alle rekenmachine opties die zijn uitgewerkt getest zijn.
End-to-End in React
Hoe zit het nu precies met hele complexe logica die verspreid is over veel componenten en de UI heel belangrijk is? Hier kun je het beste gebruik maken van end-to-end testen.
End-to-end testing is a methodology used in the software development lifecycle (SDLC) to test the functionality and performance of an application under product-like circumstances and data to replicate live settings. The goal is to simulate what a real user scenario looks like from start to finish. The completion of this testing is not only to validate the system under test, but to also ensure that its sub-systems work and behave as expected.
(What Is End-To-End Testing | Coverage At All Layers, z.d.)
Web Development studenten hebben end-to-end testen toegepast in een eerdere course. Hierbij is gebruik gemaakt van Puppeteer en Jest. Kwestie van kennis toepassen met Cucumber toch? Laten we maar eens kijken...
Als we gaan Googlen is er verrassend weinig te vinden. Laten we het zelf gaan uitzoeken op basis van eerdere opgebouwde kennis. Laten we beginnen met een feature file.
# e2e.feature
Feature: Using the Web Application e2e
Testing the application using end-to-end testing
Scenario: Browsing to the calculator
Given I open the calculator app
Then I should see the calculator
Hierboven beginnen we met een simpel scenario, namelijk de website openen en controleren of de rekenmachine zichtbaar is. We beginnen met het schrijven van een opzet. Zo ga ik alvast Puppeteer instellen en koppel ik het feature bestand aan de test script. Dit ziet er als volgt uit:
// e2e.test.js
import { defineFeature, loadFeature } from "jest-cucumber";
import puppeteer from "puppeteer";
const feature = loadFeature("./src/e2e.feature");
defineFeature(feature, (test) => {
let browser;
let page;
beforeAll(async () => {
browser = await puppeteer.launch({
headless: true,
slowMo: 50,
args: [`--window-size=1920,1080`, `--window-position=0,0`],
defaultViewport: null,
});
page = await browser.newPage();
});
// Insert step definitions here
afterAll(async () => {
await browser.close();
});
});
Hierboven is de test script voorbereid om step definities te accepteren. Eigenlijk wordt er een browser gemaakt, deze kan gebruikt worden gedurende de step definities. Zodra we klaar zijn met testen wordt de browser afgesloten. Simpel toch? Tijdens het testen maken wij gebruik van deze browser.
Zodra we dit proberen te testen verschijnt er direct een melding dat er step definities missen, laten we deze implementeren.
// e2e.test.js
test("Browsing to the calculator", ({ given, then }) => {
given("I open the calculator app", async () => {
await page.goto("http://localhost:3000");
});
then("I should see the calculator", async () => {
await page.waitForSelector("#startCalculation");
});
});
Als je al bekend bent met Puppeteer zou je precies moeten zien wat er gebeurt, we openen namelijk de applicatie op localhost:3000
. Vervolgens controleren wij of er een element bestaat met id startCalculation
. In ons geval is dat de knop waar we de berekening mee starten. Zodra de knop zichtbaar wordt is de test dus geslaagd. Jest heeft standaard een timeout ingebouwd, deze is ingesteld op 5 seconden. Mocht binnen 5 seconden de knop niet zichtbaar worden dan faalt de test. Bij testen die langer duren biedt jest de mogelijkheid aan om deze timeout te veranderen. Dit kan met jest.setTimeout
.
Vanaf nu is het eigenlijk hetzelfde als eerder behandeld. Laten we optellen en aftrekken toevoegen aan onze feature bestand. Kwestie van step definities invullen. Laten we een kijkje nemen hoe de step definities van optellen eruitziet:
test("Adding Numbers", ({ given, when, and, then }) => {
givenIOpenTheCalculatorApp(given);
whenIEnterNumbers(when);
and("I click the addition option", async () => {
await page.select("#option", "increment");
});
andIClickTheCalculateButton(and);
thenIShouldGet(then);
});
De code ziet er niet veel anders uit dan gebruikelijk. Nu is alles asynchroon omdat we een browser besturen. Hieronder staat een korte video waarbij de end-to-end test wordt uitgevoerd op basis van de feature files.
Klik hier voor een korte YouTube video om de end-to-end test in werking te zien.
De feature file en test script staan in GitLab.
Het is dus mogelijk om end-to-end tests uit te voeren met behulp van .feature
files.
Tot slot
In bovenstaande hoofdstukken wordt er beschreven hoe BDD toegepast kan worden op verschillende methode's. In alle scenario's is het gelukt om te testen met behulp van feature files. We hebben dus de volgende testen uitgevoerd:
- Unit Tests
- Integration Tests (ookwel Component Testen)
- End-to-End Tests
We hebben dus de volledige piramide kunnen testen met behulp van BDD. Iedereen kan de feature files lezen en begrijpen waar de applicatie voor dient. Na het lezen van onze feature files is het dus overduidelijk dat het om een rekenmachine webapplicatie gaat. De grootste uitdaging is natuurlijk BDD toepassen in een echt product.
Bronnen
- npm: jest-cucumber. (2021, 4 februari). Npm. https://www.npmjs.com/package/jest-cucumber
- Tucker, J. (2019, 2 januari). React Behavior Driven Development (BDD) - codeburst. Medium. https://codeburst.io/react-behavior-driven-development-bdd-535afd364e5f
- Paczoski, P. (2020, 20 maart). Running End-to-End Testing with Jest, Puppeteer, and Cucumber. Medium. https://medium.com/docplanner-tech/running-end-to-end-testing-with-jest-puppeteer-and-cucumber-8b229ecda6e2
- Facebook. (z.d.). GitHub - facebook/jest: Delightful JavaScript Testing. GitHub. Geraadpleegd op 5 oktober 2021 van https://github.com/facebook/jest
- Test Renderer –. (z.d.). React. Geraadpleegd op 6 oktober 2021 van https://reactjs.org/docs/test-renderer.html
- What Is End-To-End Testing | Coverage At All Layers. (z.d.). SmartBear. Geraadpleegd op 7 oktober 2021 van https://smartbear.com/solutions/end-to-end-testing
- Geraadpleegd op 1 november 2021 van https://cucumber.io/docs/gherkin/reference/#scenario-outline
Bronnenlijst gegenereerd met behulp van scribbr.nl.