Page
Object
Pattern
Typescript
Edition

Introduksjon

Etter at jeg publiserte min forrige artikkel om integrasjonstester med hjelp av Page Object Pattern har jeg blitt spurt om å tilby eksempeler på hvordan dette kan implementeres i React-applikasjonen. Det er en fordel å kunne samle enhetstes- og integrasjonstestene i samme mappestruktur. Når testene også er skrevet i samme programmeringssråk er det lettere å dele kode på tvers av enhets- og integrasjonstestene. I denne korte oppfølgingsartikkelen vil jeg demonstrere hvordan tilsvarende tester kan skrives i Typescript med bruk av biblioteket react-testing-library og ved bruk av Jest som testmotor. Jeg har oppdatert GitHub-repositoriet med ny kildekode.

Mer intuitive og brukervennlige tester

En av fordelene ved å bruke Page Object Pattern er at testene blir mer leselige og forståelige. De vil leses som en brukerhistorie som er bygd opp av en serie med handlinger. Personlig synes jeg dette bidrar til en lav kognitiv belastning når man forsøker å forstå hva testkoden faktisk gjør. Tekniske detaljer som spørring etter bestemte DOM-elementer vil ikke lenger måtte tolkes på overordnet nivå. Koden under inneholder samtlige tester skrevet i forbindelse med forrige artikkel, men kjøres her av Jest med hjelp av react-testing-library.

import { render } from '@testing-library/react';
import React from 'react';
import App from '../App';
import HomePageObject from './pageobjects/HomePageObject';

describe('Integration-test', () => {

  beforeEach(() => render(<App />));

  it('should verify existing todos in the table', () => {
    new HomePageObject()
      .countTodos(5)
      .verifyTodoDescriptionInTable(0, 'Create a todo app that can be tested using the Page Object Pattern')
      .verifyTodoDescriptionInTable(1, 'Conjure clever test data')
      .verifyTodoDescriptionInTable(2, 'Use Bootstrap instead of styling the example app yourself')
      .verifyTodoDescriptionInTable(3, 'Initialize git')
      .verifyTodoDescriptionInTable(4, 'Write the article');
  });

  it('should add new todo', () => {
    new HomePageObject()
      .countTodos(5)
      .clickOnAddNewTodo()
      .fillDescriptionField('A brand new TODO')
      .clickSaveButton()
      .countTodos(6)
      .verifyTodoDescriptionInTable(5, 'A brand new TODO');
  });

  it('should mark some todos as completed', () => {
    new HomePageObject()
      .countTodos(5)
      .checkboxAtRowShouldBe(0, false)
      .clickOnCompletedCheckboxAtRowNumber(0)
      .clickOnCompletedCheckboxAtRowNumber(2)
      .checkboxAtRowShouldBe(0, true)
      .checkboxAtRowShouldBe(1, false)
      .checkboxAtRowShouldBe(2, true);
  });

  it('should add new TODO but change your mind and cancel', () => {
    new HomePageObject()
      .countTodos(5)
      .clickOnAddNewTodo()
      .clickCancelButton()
      .countTodos(5);
  });

  it('should edit a TODO description', () => {
    new HomePageObject()
      .verifyTodoDescriptionInTable(0, 'Create a todo app that can be tested using the Page Object Pattern')
      .clickEditButtonAtRowNumber(0)
      .fillDescription('This description has been altered')
      .clickSaveButton()
      .countTodos(5)
      .verifyTodoDescriptionInTable(0, 'This description has been altered');
  });

  it('should delete a TODO', () => {
    new HomePageObject()
      .countTodos(5)
      .clickEditButtonAtRowNumber(0)
      .clickDeleteButton()
      .countTodos(4);
  });
});

Hver test kjører en render av hele applikasjonen som kan sees i beforeEach-bolken. Deretter initieres et nytt HomePageObject. Alle videre handlinger skjer via denne klassen. Dette inkluderer å initiere nye sider. Jeg har valgt å returnere referanse til det aktive PageObjectet etter hver handling slik at testen kan leses som en chain med handinger.

Hvordan bygge et PageObject i Typescript?

Her er et utdrag av koden til HomePageObject som er brukt i integrasjonstesten. Essensielle felter blir satt via en constructor og det kjøres en verifiseringsmetode som avbryter testen dersom disse feltene ikke eksisterer. Todo-applikasjonen som er under test har ikke behov for vente på at siden laster ferdig, men i tilfeller hvor dette er aktuelt så tilbyr react-testing-library gode verktøy for å håndtere dette. Om du ønsker kan du hygge deg med mer utdypende kode på GitHub.

class HomePageObject {

  private readonly tableBodyRows: HTMLElement[];
  private readonly addNewTodoButton: HTMLElement;

  constructor() {
    this.tableBodyRows = screen.getAllByRole('row').slice(1); // Removes the table header row
    this.addNewTodoButton = screen.getByRole('button', {name: 'Add new TODO'});
    this.verifyExistenceOfElements();
  }

  public countTodos(count: number) {
    expect(this.tableBodyRows.length).toBe(count);
    return this;
  }

  public clickOnAddNewTodo() {
    fireEvent.click(this.addNewTodoButton);
    return new AddPageObject();
  }

  public clickEditButtonAtRowNumber(rowNumber: number) {
    const editButton = this.editButtonAtRowNumber(rowNumber);
    if (!editButton) {
      fail('Could not find the edit button at row number: ' + rowNumber);
    }
    fireEvent.click(editButton);
    return new EditPageObject();
  }

  private verifyExistenceOfElements() {
    if (!this.tableBodyRows) {
      fail('The todo-table has not been initialized');
    }
    if (!this.addNewTodoButton) {
      fail('The add new todo button has not been initialized');
    }
  }
};

Funksjonene returnerer enten en referanse til seg selv om handlingen ikke forårsaker en endring av side (eller Page). I de tilfellene hvor en handling resulterer i et bytte så det returneres et nytt PageObject som vil ha sine egne DOM-elementer og metoder. I kodesnutten over vil funksjonen clickOnAddNewTodo returnere en ny instanse av AddPageObject.

class AddPageObject {

private readonly descriptionField: HTMLElement;
private readonly cancelButton: HTMLElement;
private readonly saveButton: HTMLElement;

  constructor() {
    this.descriptionField = screen.getByRole('textbox', {name: 'Description'});
    this.cancelButton = screen.getByRole('button', {name: 'Cancel'});
    this.saveButton = screen.getByRole('button', {name: 'Save'});
    this.verifyExistenceOfElements();
  }

  public fillDescriptionField(description: string) {
    fireEvent.change(this.descriptionField, {target: {value: description}});
    return this;
  }

  public clickCancelButton() {
    fireEvent.click(this.cancelButton);
    return new HomePageObject();
  }

  public clickSaveButton() {
    fireEvent.click(this.saveButton);
    return new HomePageObject();
  }

  private verifyExistenceOfElements() {
    if (!this.descriptionField) {
      fail('The description field has not been initialized');
    }
    if (!this.cancelButton) {
      fail('The cancel button has not been initialized');
    }
    if (!this.saveButton) {
      fail('The save button has not been initialized');
    }
  }
}

AddTodoPage vil også kunne sende tilbake til HomePageObject. Nå har du fått et innblikk i hvordan Page Object Pattern kan implementeres i React ved hjelp av react-testing-library. Jeg håper eksemplene over kan fungere som et godt utgangspunkt til å kunne utforske litt på egenhånd.

Avslutning

Dersom du har kommet helt ned hit så takker jeg for at du har lest artikkelen min. Jeg håper den ga verdi for deg! Dersom du lurer på noe, feedback, eller ser en feil. Ikke nøl med å kontakt meg på oya@progit.no