Testing JavaScript

Agenda

  • Testing Pyramid
  • Definitions for Frontend
  • Static Quality Checking
  • Unit + Integration Tests
    • Tools
    • Jest
    • DEMO
    • Component Testing
    • Testing Trophy
    • DOM Testing
  • E2E Testing
    • Behat + Mink
  • Ressources

Testing Pyramid

Definition for Frontend Apps

Unit Tests

individual and isolated components or functions work as expected

Integration Tests

several components work together in harmony, mock as little as possible (i.e. only mock network requests and animation libraries)

E2E Tests

A robot that clicks through the app and verifies behaviour.

Static Qualitychecks

Catch typos and type errors as you write the code

So many tools...

Static Quality checking

Code Style & Code Quality

Typos

Autofixing

These two work well together!

Type errors

Missing parameters

etc..

TSLint will be deprecated in favour of ESLint. 🎉

Unit + Integration

Jasmine

around since 2009

comes with assertions, mocking and spying tools

faster for smaller projects

Mocha + Chai + Sinon

around since 2011

Mocha = Test Runner, Chai = Assertion librar, Sinon = Stubs, Spys, Mocks

harder to set up, more configuration needed

allows for more control and customization

Jest

open sourced in 2014

everything out of the box for Vanilla- and DOM-testing (jsdom)

initially based on Jasmine => similar API

snapshot testing, parallel execution -> fast for big projects

class Cow {
  constructor(name) {
    this.name = name || "Anon cow";
  }

  greets(target) {
    if (!target) throw new Error("missing target");
    return this.name + " greets " + target;
  }
}

export default Cow;

Unit

import Cow from "../Cow";

describe("#greets", function() {
  it("should throw Error if no target is passed in", () => {
    expect(() => new Cow().greets()).toThrow(Error);
  });

  it("should greet passed target", () => {
    const greetings = new Cow("Kate").greets("Baby");
    expect(greetings).toEqual("Kate greets Baby");
  });
});

Jest

import { expect } from "chai";
import Cow from "../Cow";

describe("#greets", function() {
  it("should throw Error if no target is passed in", () => {
    expect(() => new Cow().greets()).to.throw(Error);
  });

  it("should greet passed target", () => {
    const greetings = new Cow("Kate").greets("Baby");
    expect(greetings).to.equal("Kate greets Baby");
  });
});

Mocha + Chai (+ Sinon)

import Cow from "../Cow";

describe("#greets", function() {
  it("should throw Error if no target is passed in", () => {
    expect(() => new Cow().greets()).toThrow(Error);
  });

  it("should greet passed target", () => {
    const greetings = new Cow("Kate").greets("Baby");
    expect(greetings).toEqual("Kate greets Baby");
  });
});

Jasmine

What shall be used?

Jest

Recommendation:

Reasons:

 

Easy to setup and use

 

Fast (parallel execution)

 

DOM Testing out of the box

 

Features like mocking, coverage, snapshot testing out of the box

 

Maintained and used by Facebook

 

Big and growing community

 

Jest 24k

Mocha 17k 

Jasmine 14k

Github Stars

npm trends

it('renders correctly', () => {
  const tree = renderer
    .create(<Link page="http://www.facebook.com">Facebook</Link>)
    .toJSON();
  expect(tree).toMatchSnapshot();
});
// Updated test case with a Link to a different address
it('renders correctly', () => {
  const tree = renderer
    .create(<Link page="http://www.instagram.com">Instagram</Link>)
    .toJSON();
  expect(tree).toMatchSnapshot();
});

Snapshot Testing

Snapshot Testing

USE IT WISELY!

Good use cases:

  • Error messages and logs
  • Babel plugins
  • CSS-in-JS

It is easy to hide the developers intention

using Snapshot Tests

Feature: Sum a Pair
  It sums a pair of numbers
 
  Scenario: adds 1 + 2 to equal 3
    Given 1
    When add 2
    Then the sum is 3

jest-cucumber

defineFeature(feature, test => {
  test('adds 1 + 2 to equal 3', ({ given, when, then }) => {
    let x: number;
    let z: number;
    
    given('1', () => {
      x = 1;
    });
    
    when('add 2', () => {
      z = sum(x, 2);
    });
    
    then('the sum is 3', () => {
      expect(z).toBe(3);
    });
  });
});

JSDOM

emulate enough of a subset of a web browser to be useful for testing and scraping real-world web applications.

const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`);

console.log(dom.window.document.querySelector("p").textContent); // "Hello world"

DEMO

Component Testing

Button

Image

Product

Drop-down

SizeSelector

ProductInfo

Component Testing

Component Testing

Button

Image

Product

Drop-down

SizeSelector

ProductInfo

  • components > functions
  • unit tests not very likely to break
    • isolate the component too much from the rest of the application
  • ​integration tests > unit tests

Testing Pyramid

Testing Trophy

by Kent C. Dodds

Testing Trophy

small problems

big problems

cheap

expensive

TypeScript

ESLint

dom-testing-library

Jest

Behat w/ Mink

dom-testing-library

comes in many flavours: vue-testing-library, react-testing-library,...

react-testing-library now officially recommended by the React Docs

DOM Testing

DOM Testing Library

Simple and complete DOM testing utilities that encourage good testing practices

querying DOM nodes in a way that's similar to how the user finds elements on the page​ 

// By Text
const submitButton = getByText(container, /send data/i)

// By Label Text
const inputNode = getByLabelText(container, 'Username')

// By Placeholder Text
const inputNode = getByPlaceholderText(container, 'Username')

// By Alt Text
const incrediblesPosterImg = getByAltText(container, /incredibles.*poster$/i)

// By Title
const deleteElement = getByTitle(container, 'Delete')

// By Display Value 
const lastNameInput = getByDisplayValue(container, 'Norris')

// By Role
const dialogContainer = getByRole(container, 'dialog')

// By Test ID
const usernameInputElement = getByTestId(container, 'username-input')

React Testing Library

test('loads and displays greeting', async () => {
  // ARRANGE
  const url = '/greeting'
  const { getByText, getByTestId } = render(<Fetch url={url} />)

  axiosMock.get.mockResolvedValueOnce({
    data: { greeting: 'hello there' }
  })

  // ACT
  getByText('Load Greeting').click()


  // ASSERT
  const greetingTextNode = await waitForElement(() =>
    getByTestId('greeting-text')
  )

  expect(axiosMock.get).toHaveBeenCalledTimes(1)
  expect(axiosMock.get).toHaveBeenCalledWith(url)

  expect(getByTestId('greeting-text')).toHaveTextContent('hello there')
  expect(getByTestId('ok-button')).toHaveAttribute('disabled')
});

End 2 End

Behat + Mink

  • "A robot that clicks through the app and verifies behaviour."

  • Simulate a Browser
    • Headless Browser Emulators
      • HTTP requests and emulate browser applications on a high level (HTTP stack)
    • ​In-Browser Emulators
      • real browsers, taking full control of them, using them as zombies
      • => Execute JS
  • ​​Mink = Browser Emulator Abstraction Layer

In-Browser Test

  • e.g. Feature: Wikipedia Search Field Autocompletion with JS / AJAX => In-Browser Session needed

  • Selenium2 browser emulator
@javascript
Scenario: Searching for a page with autocompletion
  Given I am on "/wiki/Main_Page"
  When I fill in "search" with "Behavior Driv"
  And I wait for the suggestion box to appear
  Then I should see "Behavior-driven development"
/**
 * @Then /^I wait for the suggestion box to appear$/
 */
public function iWaitForTheSuggestionBoxToAppear()
{
    $this->getSession()->wait(5000,
        "$('.suggestions-results').children().length > 0"
    );
}

Ressources

Links & Documentation

 

Articles

AssertJS Talks

Establishing testing patterns with software design principles

Please don't mock me

Write tests. Not too many. Mostly integration.

Working Well: The Future of JavaScript Testing

Delightful JavaScript Testing with Jest

deck

By Gerald Urschitz