Testing Library the RITE Way
RITEway shines at creating meaningful assertions. Since React is so popular, RITEway also ships with a built-in render
function to test React components. You can test pure React components with render
.
RITEway's API forces you to write good tests that answer the five questions every unit test must answer. Additionally, RITEway intentionally exposes no interface for mocking because mocking is a code smell for tight coupling. But RITEway also exposes no way to fire DOM events.
Therefore RITEway lacks the ability to create integration tests for the DOM out of the box.
One of the most popular tools for DOM integration tests is Testing Library. It makes it easy for you to avoid testing implementation details.
You are going to learn how to combine Testing Library with RITEway to write integration tests with simple and readable assertions that closely resemble how your app is used.
RITEway
Install the packages.
yarn add --dev @babel/core @babel/register babel-plugin-module-resolver eslint eslint-config-prettier eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-simple-import-sort prettier riteway tap-nirvana watch
RITEway encourages another good practice: separation of concerns through presentation (or display) and container components.
- Presentation components are responsible for what's being rendered. They are pure functions and given the same props return the same JSX.
- Container components contain the component's logic. They isolate side-effects and are usually connected to Redux and contain Hooks.
We are going to use TDD. Create the skeleton for a display component.
import React, { Fragment } from 'react';function HomePage() {return <Fragment></Fragment>}export default HomePage;
This article's example is going to be very contrived so we can focus on the essential.
Create tests for a component that displays a count and has a button to increment the count in home-page-component.test.js
.
import React from 'react';import { describe } from 'riteway';import render from 'riteway/render-component.js';import HomePage from './home-page-component.js';const createProps = ({ count = 0 } = {}) => ({ count });describe('HomePage component', async assert => {const createHomePage = (props = {}) => render(<HomePage {...props} />);{const props = createProps();const $ = createHomePage(props);assert({given: 'a count',should: 'render the count',actual: $('.count').text(),expected: props.count.toString(),});}{const props = createProps({ count: 5 });const $ = createHomePage(props);assert({given: 'a count',should: 'render the count',actual: $('.count').text(),expected: props.count.toString(),});}{const props = createProps();const $ = createHomePage(props);assert({given: 'just rendering',should: 'render the increment button',actual: $('.increment-button').length,expected: 1,});}});
If you don't use className
s and want a more robust selector, you could use $('button:contains("Increment")')
to select a button with the text "Increment".
Run yarn watch
to start the watch script. All tests should fail. Now make them pass.
import React from 'react';const HomePage = ({ count, onIncrementClick = () => {} }) => (<main><p className="count" data-testid="count">{count}</p><button className="increment-button" onClick={onIncrementClick}>Increment</button></main>);export default HomePage;
All tests should pass now. (Notice that we need the data-testid
attribute for the Testing Library tests.)
HomePage component✔ Given a count: should render the count✔ Given a count: should render the count✔ Given just rendering: should render the increment buttonpassed: 3, failed: 0 of 3 tests (1.3s)Linting ...Lint complete.
Testing Library
Install Testing Library as well as jsdom and jsdom-global. The purpose of the latter is to inject document
, window
and other DOM API into the Node.js environment.
yarn add --dev @testing-library/react jsdom jsdom-global
Amend your "test"
script to require jsdom.
"test": "NODE_ENV=test riteway -r @babel/register -r jsdom-global/register 'src/**/*.test.js'",
Next, create the skeleton for the home page's container component.
import React, { Fragment } from 'react';function HomePage() {return <Fragment></Fragment>}export default HomePage;
The integration test should test that the button click works and correctly increments the count.
import { cleanup, fireEvent, render, screen } from '@testing-library/react';import { describe } from 'riteway';import HomePage from './home-page-container.js';describe('HomePage container', async assert => {render(<HomePage />);fireEvent.click(screen.getByText(/increment/i));assert({given: "a click on the 'Increment' button",should: 'increment the count',actual: screen.getByTestId('count').textContent,expected: '1',});cleanup()});
This test should fail. Use useState
to make it pass.
import React, { useState } from 'react';import HomePageComponent from './home-page-component.js';function HomePage() {const [count, setCount] = useState(0);function handleIncrementClick() {setCount(c => c + 1);}const props = {count,onIncrementClick: handleIncrementClick,};return <HomePageComponent {...props} />;}export default HomePage;
All tests are passing now.
HomePage component✔ Given a count: should render the count✔ Given a count: should render the count✔ Given just rendering: should render the increment buttonHomePage container✔ Given a click on the 'Increment' button: should increment the countpassed: 4, failed: 0 of 4 tests (2.4s)Linting ...Lint complete.
When to Use What?
As a rule of thumb use RITEway's render
to test your pure components and RITEway with Testing Library for your container components. Isolate side-effects and test them with RITEway unit tests. Cover your app's crucial paths with E2E tests.
There is always a trade-off between using RITEway, Testing Library, and E2E testing tools (such as TestCafe or Cypress) in the form of coverage duplication, execution speed and confidence.
Tool | Pros | Cons |
---|---|---|
RITEway | - runs the fastest (basically instant after Babel compiled) - good for pure components (map props to JSX) - good for other unit tests (reducers, sagas, other pure functions etc.) | - No way to do user interactions - jQuery selectors |
Testing Library | - faster than E2E tests - ability to simulate user interactions - implementation independent selectors | - lower confidence than E2E tests - can lead to mocking hell (used wrongly) |
E2E | - actual API requests - no / few mocks - UI actually rendered in browser - ability to test cross browser functionality | - slow - sometimes flaky - difficult to write well (e.g. isolated, parallelized) |
Choose the right tool for the right job.
Note: I list jQuery selectors as a con for RITEway here because I compare them to the confidence of Testing Library's selectors. RITEway's selectors are fantastic for the job they're intended to do.
fn
Maybe you encountered an edge case where you need a Jest-like fn
function to mock something. You can define a mock function and import it in your tests.
function fn(implementation = () => {}) {const mockFn = (...args) => {mockFn.calls.push(args);return implementation(...args);};mockFn.calls = [];return mockFn;}export default fn;
You can use it to mock function calls and track with which arguments the mock has been called. Remember to properly teardown and restore your mocked functions.
import { describe } from 'riteway';import fn from './mock-fn.js';describe('fn - the mock function', async assert => {const mockedFn = fn((a, b) => a + b);assert({given: 'calling a mocked function',should: 'should return the correct result',actual: mockedFn(21, 21),expected: 42,});assert({given: "a mocked function's calls",should: 'return the correct args',actual: mockedFn.calls,expected: [[21, 21]],});});
Check out this GitHub repository, which contains the complete code.
TypeScript
yarn add --dev riteway @testing-library/react ts-node jsdom jsdom-global tap-nirvana
Make sure "compilerOptions"
is set to "commonjs"
and "jsx"
is set to "react"
in your tsconfig.json
.
{"test": "NODE_ENV=test riteway -r jsdom-global/register -r ts-node/register/transpile-only 'src/**/*.test.{ts,tsx}' | tap-nirvana"}