Skip to content

Jan Hesters

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 classNames 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 button
passed: 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 button
HomePage container
✔ Given a click on the 'Increment' button: should increment the count
passed: 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.

ToolProsCons
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"
}