|

Unit-Testing mit Puppeteer

Im Bereich Continuous Integration (CI) benötigt man oft Unit-Tests die bei jedem git push Befehl ausgeführt werden und somit sichergestellt werden kann, dass noch alles wie bisher funktioniert und keine Regressions eingeführt werden.

Diese müssen in einer entsprechenden Umgebung ausgeführt werden, zum Beispiel einem Browser.

Bislang hat man dafür oft PhantomJS verwendet. PhantomJS wird jedoch nicht mehr aktiv entwickelt und hat einige Bugs und Probleme.

Seit Chrome 59 gibt es jedoch einen sehr aktiv entwickelten Browser der als Headless Chrome für automatisierte Unit-Tests verwendet werden kann.

Die meisten Testrunner, zum Beispiel Karma, verwenden die bereits installierten Browser.

Wir bevorzugen jedoch Puppeteer, was eine API für Headless Chrome zur Verfügung stellt und standardmäßig eine aktuelle Version von Chromium direkt mitliefert und somit unabhängig von den installierten Browsern ist.

Als Test-Framework verwenden wir Mocha und Chai als Assertion-Library.

mkdir puppeteer-unit-testing && cd puppeteer-unit-testing
npm init -y
npm i mocha mocha-headless-chrome chai -D

Anschließend erstellen wir die benötigte Standard-Struktur

mkdir test
touch test/test.js && touch test/test.html

test/test.html wird für das Ausführen der Unit-Tests im Browser benötigt. Beim Inhalt der Datei orientieren wir uns an dem Beispiel aus der Dokumentation von Mocha.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <link href="../node_modules/mocha/mocha.css" rel="stylesheet" />
</head>
<body>
    <div id="mocha"></div>
    <script>window.browser_based = true</script>
    <script src="../node_modules/chai/chai.js"></script>
    <script src="../node_modules/mocha/mocha.js"></script>
    <script>mocha.setup('bdd')</script>
    <script src="test.js"></script>
    <script>
    mocha.run();
    </script>
</body>
</html>

Das Template kann man sehr einfach zum Beispiel über Emmet in VSCode generieren.

Wir setzen jedoch noch eine Variable browser_based auf dem window Objekt auf den Wert true, damit wir gezielt zwischen der Browserumgebung und Node.js im Terminal unterscheiden können.

Anschließend erstellen wir 2 kleine Unit-Tests in test/test.js.

'use strict'

const assert = this.chai ? this.chai.assert : require('assert')

if(typeof window === 'object' && window.browser_based){
  describe('browser tests', function () {
    describe('document', function () {
      describe('#querySelectorAll().length', function () {
        it('should return 1 when the element is present', function () {
          assert.equal(document.querySelectorAll('#mocha').length, 1)
        })
      })
    })
  })
}

describe('Array', function() {
  describe('#indexOf()', function() {
    it('should return -1 when the value is not present', function() {
      assert.equal([1,2,3].indexOf(4), -1)
    })
  })
})

Dort greifen wir entweder auf die assert Methode von Chai zu (falls wir in der Browserumgebung sind) oder verwenden das native assert Modul von Node.js.
const assert = this.chai ? this.chai.assert : require('assert')

Anhand einer Prüfung von window und window.browser_based können wir festlegen, welche Tests zustzlich in der Browserumgebung laufen.
typeof window === 'object' && window.browser_based

Wir definieren noch 2 kurze npm Scripts in der package.json, damit wir entweder die Tests über Node.js oder den Browser einfach ausführen können.

"scripts": {
  "test": "mocha",
  "test-browser": "mocha-headless-chrome -f test/test.html"
},

Nun können die Tests über npm test bzw. npm run test und npm run test-browser ausgeführt werden.

Mit dieser Konfiguration kann man nun alle Unit-Tests für Browser und Node.js einfach umsetzen.

Ein nächster möglicher Schritt wäre die Einrichtung von Git-Hooks, zum Beispiel per Husky, damit bei fehlschlagenden Unit-Tests fehlerhafter Code nicht per git commit und git push veröffentlicht wird.

Jest statt Mocha

Es gibt von Facebook das Test-Framework Jest, was ebenfalls mit Puppeteer genutzt kann. Um dies zu vereinfachen gibt es jest-puppeteer.

Um nicht erneut unsere Tests zu schreiben gibt es die Möglichkeit diese einfach per jest-codemods für Jest zu portieren.

Wir bereiten die aktuellen Testdateien vor und portieren diese dann für Jest.

cp test/test.js test/jest.test.js
cp test/test.html test/jest.test.html

Nun installieren wir die benötigten Abhängigkeiten.

npm i jest-puppeteer puppeteer jest jest-codemods -D
./node_modules/.bin/jest-codemods

Bei jest-codemods müssen folgende Optionen ausgewählt werden:

Mocha
No, use explicit require() calls instead of globals
Chai: Assert Syntax
On which files or directory should the codemods be applied? test/jest.test.js

Den Inhalt von jest.test.html passen wir wie folgt an:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="jest"></div>
</body>
</html>

Den Inhalt von jest.test.js passen wir ebenfalls etwas an:

'use strict'

const assert = require('assert')
const path = require('path')
//const expect = require('expect')

const browser = process.argv.includes('--browser')

if(browser){
  beforeAll(async () => {
    await page.goto(`file:${path.join(__dirname, 'jest.test.html')}`)
  })
  describe('browser tests', () => {
    describe('document', () => {
      describe('#querySelectorAll().length', () => {
        test('should return 1 when the element is present', async () => {
          const currentValue = await page.evaluate(
            () => document.querySelectorAll('#jest').length
          )
          assert.equal(currentValue, 1)
        })
      })
    })
  })
}


describe('Array', () => {
  describe('#indexOf()', () => {
    test('should return -1 when the value is not present', () => {
      assert.equal([1,2,3].indexOf(4), -1)
    })
  })
})

Wir erstellen die Config für jest-puppeteer:

touch jest.config.js
module.exports = {
    preset: 'jest-puppeteer'
};

Und erweitern die package.json:

"test": "mocha",
+"test:jest": "jest jest.test.js",
"test-browser": "mocha-headless-chrome -f test/test.html -r nyan",
+"test-browser:jest": "jest jest.test.js --browser"

npm run test:jest und npm run test-browser:jest führen die Tests mit Jest und Puppeteer aus.

Den gesamten Code gibt es hier nochmal.

Es gibt einige hilfreiche Migrations-Anleitungen um zu Jest zu migrieren, zum Beispiel von Mocha zu Jest.

eBay und Airbnb haben dazu auch ein paar Artikel.

Ein paar Gründe für die Migration sind zum Beispiel hier beschrieben.

Happy testing!

weitere Anwendungsfälle

Auf der Google I/O 2018 wurden von Eric Bidelman weitere Beispiele vorgestellt, was man mit Puppeteer alles machen kann. Darunter SSR bzw. Prerendering von JavaScript-Seiten, weitere Tests (Code-Coverage, A/B, Chrome Extensions, Offline Caching), PDF Generierung, Crawling und weitere Lösungen.