Testing Frontend Applications

Posted on:

Basics of tooling, best practices and how to get started testing frontend applications

Relevant Links


Testing Frontend Applications

Another CodingWithCallum™️ Session


This presentation was written using obsidian slides which is why the markdown looks a little weird, see attached PDF output and notes of every slide.

Everything underneath "note" below are speaker notes[[Server Components Slides]] to remind me what to talk about

Who are you?

Callum Silcock

Experience Engineering > Bluestone Platform (Web)

https://csi.lk || github.com/csi-lk


Have been doing Front end development for over a decade

Love hate relationship with testing

What is this talk?

  • Testing (right?... right)
  • Tooling, best practices
  • Very few images (no gifs, sorry)
  • Interactive!
  • Opinionated


There are many kinds of tooling, these are the ones I suggest (+ some we already decided on)

Ask questions! Inturrupt! Call out when things don't make sense, if you're remote throw things in the chat

All of this is my opinion based on how i've seen testing work at 100+ frontend departments

Ok lets'a'go!

Write tests. Not too many. Mostly integration.

~ Guillermo Rauch (CEO of Vercel)


Ok what does this mean?

The Testing Trophy

Higher up the trophy = more $$$ / better confidence


You'll see a bunch of stuff I've stolen from Kent C Dodds in this talk Ok the basic idea is, the higher up the trophy you go, the more expensive it is in terms of Development effort and Runtime But the higher up the trophy we go, the better outcomes we get from our testing So the best ROI we can get on our testing is in the middle of the trophy at the integration layer But lets walk the trophy from bottom to top


Catch typos and type errors as you write the code

Pretty straightforward....


everyone loves static


  • ESlint
  • Typescript
  • Prettier

Static Example

// can you spot the bug?
// ELint's for-direction rule can 😉
for (var i = 0; i < 10; i--) {
const two = '2'
// ok, this one's a bit contrived,
// but TypeScript will tell you this is bad:
const result = add(1, two)


ref: for-direction


Verify that individual, isolated parts work as expected.

  • Our quickest tests that require some writing


unit tests should be simplistic, a+b=c unit tests are great for backend but not great for frontend development that requires user interaction in a browser


  • Jest
    • Mocks
    • Snapshots
  • React Testing Library
  • React Hooks Testing Library

Unit example

test('renders "no items" when the item list is empty', () => {
  render(<ItemList items={[]} />);
  expect(screen.getByText(/no items/i)).toBeInTheDocument();
test('renders the items in a list', () => {
  render(<ItemList items={['apple', 'orange', 'pear']} />);
  // could use a snapshot test but use toMatchInlineSnapshot();
  expect(screen.queryByText(/no items/i)).not.toBeInTheDocument();


  • Where the most tests will be written
  • Should not mock dependencies
    • allows for automated updating of dependencies from Rennovate
  • Should only mock browser based pieces (eg. server APIs)


  • Jest
  • React Testing Library
  • React Hooks Testing Library
  • User Event
  • Mock Service Worker
  • ShellJS
  • @jackfranklin/test-data-bot

Integration Example

import * as React from 'react'
import {render, screen, waitForElementToBeRemoved} from 'test/app-test-utils'
import userEvent from '@testing-library/user-event'
import {build, fake} from '@jackfranklin/test-data-bot'
import {rest} from 'msw'
import {setupServer} from 'msw/node'
import {handlers} from 'test/server-handlers'
import App from '../app'
const buildLoginForm = build({
  fields: {
    username: fake(f => f.internet.userName()),
    password: fake(f => f.internet.password()),

const server = setupServer(...handlers)
beforeAll(() => server.listen())
afterAll(() => server.close())
afterEach(() => server.resetHandlers())
test(`logging in displays the user's username`, async () => {
  await render(<App />, {route: '/login'})
  const {username, password} = buildLoginForm()
  userEvent.type(screen.getByLabelText(/username/i), username)
  userEvent.type(screen.getByLabelText(/password/i), password)
  userEvent.click(screen.getByRole('button', {name: /submit/i}))
  await waitForElementToBeRemoved(() => screen.getByLabelText(/loading/i))


I split this into three sections because it makes it easier to read and I couldn't get it to format correctly in one big one

End to End

A helper robot that behaves like a user to click around the app and verify that it functions correctly. Sometimes called "functional testing" or e2e.

  • Should only cover p0/p1 flows
  • Does not mock backend, runs against real data usually in an environment


  • playwright
    • what bluestone is using, (documented in bluestone-hello)
  • cypress

E2E Code Example

import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
import { BASE_URL } from "../config";
test("homepage has title and a button", async ({ page }) => {
  await page.goto(BASE_URL);
  await expect(page).toHaveTitle("ANZx – Join app!");
  await expect(page).toHaveURL(new RegExp(`^${BASE_URL}`));
  expect(page.locator(`button:has-text('click click')`)).toBeDefined;
test("homepage handles accessibility", async ({ page }) => {
  await page.goto(BASE_URL);
  const accessibilityScanResults = await new AxeBuilder({ page }).analyze();

What are we missing?


ok we've made it through the testing trophy what's next

Accessibility (a11y)

Automation tools can only get us 30% of the way toward being WCAG compliant (ref; UK Gov)


but better than nothing


  • jest-axe
  • deque tooling
  • axe-core/playwright


  • Blackduck (dependency scanning)
  • Twistlock (container scanning)
  • Sonarqube (high level issues like xss etc.)

Live Demo 😱

Suggested Reading

Kent C Dodds' blog (where I stole found this content)