Automatisiertes Testen in JavaScript

Automatisiertes Testen

Code kann automatisch getestet werden, um sicherzustellen, dass er wie erwartet funktioniert

Tools für das Testen

  • Runtime: node
  • Test Runner: node:test (seit node 18), jest, mocha
  • Assertion Libraries: assert (in node beinhaltet), jest, chai

Popularität:

Einfaches Beispiel: shorten

Wir werden eine Funktion schreiben und testen, die einen String auf eine vorgegebene Länge kürzt:

shorten('loremipsum', 6);
// should return 'lor...'

Einfaches Beispiel: shorten

Implementierung, die getestet werden soll:

/**
 * shortens a given string to a specified length,
 * adding "..." at the end if it was shortened
 */
function shorten(text, maxlength) {
  if (text.length > maxlength) {
    return text.slice(0, maxlength - 3) + '...';
  }
  return text;
}
export default shorten;

Beispiel: shorten

einfache Tests:

// shorten.test.js
import assert from 'assert/strict';
import shorten from './shorten';

assert.equal(shorten('loremipsum', 4), 'l...');
assert.equal(shorten('loremipsum', 9), 'loremi...');
assert.equal(shorten('loremipsum', 10), 'loremipsum');
assert.equal(shorten('loremipsum', 11), 'loremipsum');

assert.equal wirf eine Exception, wenn die Bedingung nicht erfüllt wird

Assertions

Assertions

Assertions können auf verschiedene Arten geschrieben werden:

assert:

assert.equal(a, b);

expect (Beispiel aus Jest):

expect(a).toEqual(b);

should (Beispiel aus Cypress):

inputField.should('have.value', '');

Assertions in node.js

assert (node):

assert.equal(a, b);
assert.deepEqual(a, b);
assert.throws(() => JSON.parse(''));
// ...

Assertions mit Chai

expect(a).to.equal(4);
expect(a).not.to.equal(2);
expect(a).to.be.greaterThan(3);
expect(a).to.be.a('number');
expect(() => JSON.parse('')).to.throw();

Assertions mit Jest

expect(a).toEqual(4);
expect(a).not.toEqual(2);
expect(a).toBe(4);
expect(a).toBeGreaterThan(3);
expect(a).toBeInstanceOf(Number);
expect(() => JSON.parse('')).toThrow();

Assertions

"deep equality": vergleicht Inhalte con Objekten / Arrays

assert.deepEqual([1, 2], [1, 2]);
expect([1, 2]).to.eql([1, 2]);
expect([1, 2]).toEqual([1, 2]);

"strict equality": verhält sich wie === - kann zum Vergleichen von Primitiven verwendet werden (oder zum Identitätsvergleich bei Objekten)

assert.equal('abc', 'abc');
expect('abc').to.equal('abc');
expect('abc').toBe('abc');

Test Runner

Test Runner

  • finden Testdateien
  • führen Tests aus
  • Erstellen Berichte über Testresultate

Verbreitete Test Runner

  • Jest (beinhaltet Assertion-Tools)
  • Mocha (oft gemeinsam mit Chai verwendet)
  • node:test

Ausführen von Tests

Tests werden meist mittels eines npm Scripts ausgeführt - z.B. via npm run test (oder abgekürzt npm test)

Finden von Tests

Jest: sucht standardmäßig nach Dateien in __tests__-Ordnern und nach Dateien, die mit .test.js oder .spec.js enden

Mocha: sucht standardmäßig nach Dateien in dem Ordner test (eigenes Muster z.B. via: mocha "src/**/*.{test,spec}.{js,jsx}")

Definieren von Tests

Die Definition eines Tests beinhaltet üblicherweise:

  • einen String, der den Test beschreibt
  • eine Funktion, die den Testcode ausführt

Tests werden üblicherweise durch einen Aufruf von test() oder it() definiert

Definieren von Tests

Beispiel mit den eingebauten Tools von node:

test(
  'shortens "loremipsum" to "lor..." with limit 6',
  () => {
    const shortened = shorten('loremipsum', 6);
    assert.equal(shortened, "lor...");
  }
)

Gruppierung von Tests

Tests können in Gruppen (und Untergruppen, ...) organisiert werden

Gruppieren von Tests

Guppierung von Tests mittels node:test:

test('strings that are short enough', (t) => {
  // t: test context
  t.test('leaves "abc" unchanged with limit 3', () => {
    assert.equal(shorten('abc', 3), 'abc');
  });
  t.test('leaves "a" unchanged with limit 1', () => {
    assert.equal(shorten('a', 1), 'a');
  });
});

Gruppieren von Tests

Gruppieren von Tests mit Jest:

describe('strings that are short enough', () => {
  test('leaves "abc" unchanged with limit 3', () => {
    expect(shorten('abc', 3)).toEqual('abc');
  });
  test('leaves "a" unchanged with limit 1', () => {
    expect(shorten('a', 3)).toEqual('a');
  });
});

Testabdeckung

Manche Testlibraries können berichten, wie viel des Codes von Tests abgedeckt ist:

npx jest --coverage

in einem create-react-app Projekt:

npm test -- --coverage

Setup und teardown

Für Code, der vor bzw. nach jedem Test in einer Gruppe ausgeführt werden soll:

describe('database', () => {
  beforeEach(() => {
    createTestDB();
  });
  afterEach(() => {
    clearTestDB();
  });

  it(/*...*/);
  it(/*...*/);
});

Ãœbung

schreibe Tests, die das Verhalten der String-Methode .replace() testen

End-to-end Tests

End-to-end Tests

End-to-end Tests: ein Browser wird gesteuert, um mit einer Website zu interagieren und ihr Verhalten zu verifizieren

End-to-end Tests

Tools:

  • selenium
  • cypress: 2017 veröffentlicht
  • puppeteer: unterstützt nur Chrome / Chromium, 2017 veröffentlicht
  • playwright: Fork von puppeteer, 2020 veröffentlicht

End-to-end Tests mit Cypress

Cypress

verwendet im Hintergrund mocha und chai

Setup

zum Initialisieren, führe npx cypress open aus

(erstelle Ordner cypress und Datei cypress.config.js im aktuellen Verzeichnis)

Ausführen

Öffnen der grafischen Oberfläche:

npx cypress open

Ausführen aller Tests im Terminal:

npx cypress run

Cypress

Beispiel: Testen von Wikipedia:

in cypress/e2e/wikipedia.cy.js:

describe('wikipedia', () => {
  beforeEach(() => {
    cy.visit('https://en.wikipedia.org');
  });

  it("page title includes 'wikipedia'", () => {
    cy.title().should('match', /wikipedia/i);
  });
});

Cypress

globales Abfragen von Elementen:

  • erstes Element nach Selektor und Text: cy.contains("h1", "hello world")
  • erstes Element nach Text: cy.contains("hello world")
  • alle nach Selektor: cy.get("main #name-input")
  • erstes Element nach Selektor: cy.get("main #name-input").first()

Cypress

Abfragen von Unterelementen:

  • .find(): ähnlich zu .get(), aber für Unterelemente
  • .contains()
cy.contains('li', 'TODO: write tests').find('button');
cy.contains('li', 'TODO: write tests').contains(
  'button',
  'delete'
);

Cypress

Abfragen des Elternelements:

cy.contains('h1', 'section title').parent();

Cypress

Interagieren mit Elementen:

  • cy.get("#reset-btn").click()
  • cy.get("#name-input").type("foo")
  • cy.get("#name-input").type("{esc}")

Cypress

Beispiel für eine Assertion:

cy.get('#name-input').should('have.class', 'invalid');

andere Assertions:

  • cy.url().should("match", /\/items\/d+$/)
  • .should("have.class", "invalid")
  • .should("have.value", "")
  • .should("be.visible")

Cypress

Beispiel: Suche auf Wikipedia

it('search', () => {
  cy.get('#searchInput').type('cypress');
  cy.get('#searchButton').click();
  cy.get('p').first().should('include.text', 'Cypress');
});

Ãœbung

Schreibe Tests für die Todo-Anwendung auf:

https://do49e.csb.app/

End-to-end Tests mit puppeteer

Puppeteer

npm-Pakete:

  • puppeteer
  • @types/puppeteer

wir werden jest als Test-Runner verwenden

Puppeteer

Testen von Wikipedia:

import puppeteer from 'puppeteer';

test('wikipedia title', async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://en.wikipedia.org');
  const pageTitle = await page.title();
  expect(pageTitle).toMatch(/Wikipedia/);
  await browser.close();
});

Puppeteer

Restrukturierung für mehrere Tests:

let browser: puppeteer.Browser;
let page: puppeteer.Page;
beforeAll(async () => {
  browser = await puppeteer.launch();
});
beforeEach(async () => {
  page = await browser.newPage();
  await page.goto('https://en.wikipedia.org');
});
afterAll(async () => {
  await browser.close();
});

test('wikipedia title', async () => {
  const pageTitle = await page.title();
  expect(pageTitle).toMatch(/Wikipedia/);
});

Puppeteer

Test, der tatsächlich ein Browser-Fenster öffnet:

beforeAll(async () => {
  browser = await puppeteer.launch({ headless: false });
});

Puppeteer

Abfragen von Elementen ist nicht trivial, da wir mit zwei separate JavaScript-Umgebungen arbeiten (node und Chromium)

Seiteninhalte abfragen:

  • page.$eval() für Inhalte eines einzelnen Elements
  • page.$$eval() zum Abfragen aller zutreffenden Elemente

Elemente abfragen, um mit ihnen zu interagieren:

  • page.$() für ein einzelnes Element
  • page.$$() für ein Array aller zutreffenden Eelmente

Puppeteer

Elemente abfragen, um auf deren Inhalte zuzugreifen:

const firstLinkText = await page.$eval(
  'a',
  (element) => element.innerHTML
);

const thirdLinkText = await page.$$eval(
  'a',
  (elements) => elements[2].innerHTML
);

Puppeteer

Elemente abfragen, um mit ihnen zu interagieren:

const firstLink = await page.$('a');
await firstLink.click();
await page.waitForNavigation();

Puppeteer

Beispiel: Suche auf Wikipedia

test('wikipedia search', async () => {
  await page.click('#searchInput');
  await page.keyboard.type('puppeteer');
  await page.click('#searchButton');
  await page.waitForNavigation();
  const paragraphText = await page.$eval(
    'p',
    (element) => element.textContent
  );
  console.log(paragraphText);
  expect(paragraphText).toMatch(/puppeteer/i);
});

Bemerkungen: page.keyboard.press("Enter") würde eine Volltextsuche auslösen; auf manchen Wikipedia-Seiten ist der erste Paragraph leer.

Puppeteer

wichtige Methoden:

  • page.$()
  • page.$$()
  • page.$eval()
  • page.$$eval()
  • page.keyboard.type("abc")
  • page.keyboard.press("Enter")
  • page.click("#selector")
  • element.click()
  • page.waitForNavigation()

vollständiges API

Ãœbung

schreibe Tests für die Todo-App unter:

https://do49e.csb.app/

End-to-end tests mit Playwright

Playwright

beinhaltet expect aus Jest

Setup

npm install @playwright/test

Installiere Browser (Chromium, Firefox, Webkit):

npx playwright install

Ausführen von Tests

Ausführen von Tests:

npx playwright test

Ausführen im "headed"-Modus (öffnet Browser-Fenster):

npx playwright test --headed

Playwright

Testen von Wikipedia:

import { test, expect } from '@playwright/test';

test('wikipedia title', async ({ page }) => {
  await page.goto('https://en.wikipedia.org');
  const pageTitle = await page.title();
  expect(pageTitle).toMatch(/Wikipedia/);
});

Playwright

Umstrukturieren des Codes für mehrere Tests:

import { test, expect } from '@playwright/test';

test.beforeEach(async ({ page }) => {
  await page.goto('https://en.wikipedia.org');
});

test('wikipedia title', async ({ page }) => {
  const pageTitle = await page.title();
  expect(pageTitle).toMatch(/Wikipedia/);
});

Playwright

Klicken auf einen Link:

await page.click('text=About Wikipedia');

Playwright

Beispiel: Suchen auf Wikipedia

test('wikipedia search', async ({ page }) => {
  await page.fill(
    'input[aria-label="Search Wikipedia"]',
    'foo'
  );
  await page.click('#searchButton');
  await page.waitForNavigation();
  const mainHeading = page.locator('h1');
  await expect(mainHeading).toHaveText(/foo/i);
});

Bemerkungen: page.keyboard.press("Enter") würde eine Volltextsuche auslösen

Mocking mit Jest

Mocking

Mocking: Simulieren von Objekten / Interfaces in einer Test-Umgebung

Mocking

Beispiel: Mocken einer Netzwerkanfrage

import fetchMock from 'fetch-mock';

fetchMock.mock('https://example.com', { foo: 'bar' });

Mocking von Modulen

Mocken eines Moduls mittels jest.mock:

jest.mock('axios', () => ({
  get: () => Promise.resolve({ data: { foo: 'bar' } }),
}));

Mocking von Modulen

Mocking von Modulen via __mocks__ Ordnern:

__mocks__/fs.js
__mocks__/axios.js
src/foo.js
src/foo.test.js
src/__mocks__/foo.js
// src/foo.test.js
jest.mock('fs');
jest.mock('axios'); // optional for contents of node_modules
jest.mock('./foo');

Bemerkung: in einem create-react-app-Projekt wäre dies z.B. src/__mocks__/axios.js anstatt __mocks__/axios.js (siehe issue)

Mocken und Inspizieren von Funktionen

Mocken einer Funktion, die aufgerufen und später inspiziert werden kann:

const mockFn = jest.fn();
mockFn('foo');
expect(mockFn).toHaveBeenCalledWith('foo');

Ressourcen