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
- Headless Browser Emulators
- 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
deck
- 356