Testing React hooks and components
This lesson introduces many things — React Testing Library, Jest, MSW, and we'll get a sneak peek of testing React Components and React hooks.
We implemented a custom React hook in the previous lessons and tested it manually from within a sample application. However, we want to ensure that it's working as expected without trying it manually after every change to the front-end application. We want to make sure that the development of the application won't break existing functionality. That's why we're going to cover testing React components in this lesson.
We can find the following recommendation in theReact docs:
If you need to test a custom Hook, you can do so by creating a component in your test and using your Hook from it. Then you can test the component you wrote. To reduce the boilerplate, we recommend usingReact Testing Library which is designed to encourage writing tests that use your components as the end-users do.
The React Testing Library allows us to test React components smoothly. It comes with utility functions that empower us to write maintainable and thorough tests. As Testing Library is only a testing utility, we also need a test runner. We'll use Jest for that.
Installing dependencies#
So, let's get to the setup part. We need to install a few things:
jest
— the test runner.@types/jest
— type declarations for Jest.ts-jest
:ts-jest is a Jest transformer with source map support that lets you use Jest to test projects written in TypeScript.
@testing-library/react
— testing utilities for React components. Docs:https://react-hooks-testing-library.com/.@testing-library/user-event
— it provides an advanced simulation of browser interactions.node-fetch
— our tests will be run bynode
, but the Fetch API is not implemented there. Thus, we need an external module to handle fetch requests.
yarn add jest @types/jest ts-jest @testing-library/react @testing-library/user-event node-fetch
We're also going to add a new script in the package.json
so that we can typeyarn test
in the terminal, and Jest will run all the tests:
"scripts": {
"test": "jest"
},
The last step for setting the test runner is adding a Jest configuration. For
that, we'll create a jest.config.js
file in the root directory, and we'll set
the following settings:
preset: 'ts-jest
,testEnvironment: 'jsdom'
— The default environment in Jest is thenode
environment. However, we have a web application, so we need a browser environment.globals: { 'ts-jest': { tsconfig: { jsx: 'react-jsx' }}}
— we're going to use JSX in our test, so we need to letts-jest
know how it should transform JSX syntax.setupFilesAfterEnv: ['./test/setup.ts'],
— we want Jest to run a custom setup steps before running the tests.
module.exports = {
preset: "ts-jest",
testEnvironment: "jsdom",
setupFilesAfterEnv: ["./test/setup.ts"],
globals: {
"ts-jest": {
tsconfig: {
jsx: "react-jsx",
},
},
},
};
We also need to create thesetup.ts
file. In there, we'll put the following
code:
if (!global.fetch) {
global.fetch = require("node-fetch");
}
As we mentioned before, fetch
doesn't exist in node
, so we need to declare
that it should use the node-fetch
module instead.
Testing React Hooks#
When you look at the useComments
hook we implemented, you may notice that it'sjust a function. It may be tempting to call it and test the result. However,
we can't do that for the following reasons:
It's not a pure function. Our hook has side effects (API calls), so the test result is not solely dependant on the parameter, but it relies on the outside world — the API.
We would break the rules of hooks — hooks are only supposed to be called inside of a function component. If we call it outside of a component, we'll get an error:
1Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
21. You might have mismatching versions of React and the renderer (such as React DOM)
32. You might be breaking the Rules of Hooks
43. You might have more than one copy of React in the same app
56See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.
What's more — the hook is intended to be used inside a component, so testing it outside won't cover its functionality. Ideally, we should always test things in the way they are used.
We will test the whole comments section in this course — we'll write a test for both the hook behavior and the component behavior.
Previously, we wrote the whole code in [slug].tsx file
. Let's extract the
comments section to a separate component. It will accept the postSlug
prop and
call the useComments
hook.
import { useState } from "react";
import { useComments } from "../lib/useComments";
export interface PostCommentsProps {
postSlug: string;
}
export const PostComments = ({ postSlug }: PostCommentsProps) => {
const { comments, error, loading, addComment } = useComments(
"https://newline-guide.hasura.app/v1/graphql",
postSlug
);
const [username, setUsername] = useState("");
const [comment, setComment] = useState("");
return (
<section className="pt-6 max-w-2xl mx-auto">
<form
className="mb-6"
onSubmit={(e) => {
e.preventDefault();
addComment({ author: username, content: comment });
}}
>
<div className="mb-4">
<label
className="block text-gray-600 text-sm font-bold mb-2"
htmlFor="username"
>
Name
</label>
<input
id="username"
className="border rounded w-full py-2 px-3 text-gray-700"
type="text"
placeholder="Jon Snow"
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="mb-4">
<label
className="block text-gray-600 text-sm font-bold mb-2"
htmlFor="comment"
>
Name
</label>
<textarea
name="comment"
className="border rounded w-full py-2 px-3 text-gray-700"
placeholder="Tell me what you think..."
onChange={(e) => setComment(e.target.value)}
/>
</div>
<button
className="bg-purple-700 hover:bg-purple-600 text-white font-bold py-2 px-4 rounded"
type="submit"
>
Add comment
</button>
</form>
<h3 className="font-bold text-gray-500">{count} comments</h3>
{loading
? "Loading comments..."
: comments.map(({ author, content, created_at, status }) => (
<article key={created_at} className="bg-gray-100 rounded my-6 p-4">
<div className="font-bold mb-2">
{author} {new Date(created_at).toLocaleDateString()}
{status ? ` ・ ${status}` : null}
</div>
<p className="text-gray-700">{content}</p>
</article>
))}
</section>
);
};
We'll use it in the Post
component in place of the previous code:
<PostComments postSlug={post.slug} />
This page is a preview of The newline Guide to Full Stack Comments with Hasura and React