When writing applications, testing is crucial for ensuring code behaves as expected. In this guide, you'll learn how to get started quickly writing tests using TypeScript, React, and Jest in an idiomatic way.
There are several benefits to leveraging TypeScript when testing:
Writing unit tests and using TypeScript are not mutually exclusive, when used together they help you build more maintainable codebases.
We'll be using Jest, a popular test framework for JavaScript projects including React.
Jest will take care of running tests and handling assertions but since we are testing React, we will need some testing utilities.
There are two popular testing libraries for React: Enzyme and React Testing Library.
In this guide we will be testing React components using React Testing Library, as it provides a simple and straightforward way to test components that promotes good test practices.
There are several practices that the React Testing Library promotes:
These two practices help focus tests on behavior and user interaction, treating the internals of a component like a "black box" that shouldn't be exposed.
"The more your tests resemble the way your software is used, the more confidence they can give you." -- Kent C. Dodds, creator of RTL.
Starting with an existing React and TypeScript project, we can add dependencies for Jest and React Testing Library:
1npm install @types/jest @testing-library/react @testing-library/jest-dom jest ts-jest
This installs Jest and React Testing Library with TypeScript support.
Add a new jest.config.js
file to the root of your project:
1module.exports = {
2 // The root of your source code, typically /src
3 // `<rootDir>` is a token Jest substitutes
4 roots: ["<rootDir>/src"],
5
6 // Jest transformations -- this adds support for TypeScript
7 // using ts-jest
8 transform: {
9 "^.+\\.tsx?$": "ts-jest"
10 },
11
12 // Runs special logic, such as cleaning up components
13 // when using React Testing Library and adds special
14 // extended assertions to Jest
15 setupFilesAfterEnv: [
16 "@testing-library/react/cleanup-after-each",
17 "@testing-library/jest-dom/extend-expect"
18 ],
19
20 // Test spec file resolution pattern
21 // Matches parent folder `__tests__` and filename
22 // should contain `test` or `spec`.
23 testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
24
25 // Module file extensions for importing
26 moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"]
27};
This is a typical Jest configuration but with some additional modifications:
ts-jest
packageAdd a new npm
script to package.json
:
1{
2 ...
3 "scripts": {
4 ...
5 "test": "jest",
6 "test:watch": "jest --watch"
7 }
8}
Jest supports a powerful "watch" mode that will re-run changed tests in a quick way while you develop.
Tests should be organized into __tests__
folders under a top-level src
directory, according to this configuration.
Ensure your tsconfig.json
has the esModuleInterop
flag enabled for compatibility with Jest (and Babel):
1{
2 "compilerOptions": {
3 "esModuleInterop": true
4 }
5}
To ensure the additional Jest matchers are available for all test files, create a src/globals.d.ts
and import the matchers:
1import "@testing-library/jest-dom/extend-expect";
For this guide, we'll test a basic component that has an internal state and uses React hooks to showcase how you'd write a set of tests.
Create src/LoginForm.tsx
that contains the following:
1import React from "react";
2
3export interface Props {
4 shouldRemember: boolean;
5 onUsernameChange: (username: string) => void;
6 onPasswordChange: (password: string) => void;
7 onRememberChange: (remember: boolean) => void;
8 onSubmit: (username: string, password: string) => void;
9}
10
11function LoginForm(props: Props) {
12 const [username, setUsername] = React.useState("");
13 const [password, setPassword] = React.useState("");
14 const [remember, setRemember] = React.useState(props.shouldRemember);
15
16 const handleUsernameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
17 const { value } = e.target;
18 setUsername(value);
19 props.onUsernameChange(value);
20 };
21
22 const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
23 const { value } = e.target;
24 setPassword(value);
25 props.onPasswordChange(value);
26 };
27
28 const handleRememberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
29 const { checked } = e.target;
30 setRemember(checked);
31 props.onRememberChange(checked);
32 };
33
34 const handleSubmit = (e: React.FormEvent) => {
35 e.preventDefault();
36 props.onSubmit(username, password);
37 };
38
39 return (
40 <form data-testid="login-form" onSubmit={handleSubmit}>
41 <label htmlFor="username">Username:</label>
42 <input
43 data-testid="username"
44 type="text"
45 name="username"
46 value={username}
47 onChange={handleUsernameChange}
48 />
49
50 <label htmlFor="password">Password:</label>
51 <input
52 data-testid="password"
53 type="password"
54 name="password"
55 value={password}
56 onChange={handlePasswordChange}
57 />
58
59 <label>
60 <input
61 data-testid="remember"
62 name="remember"
63 type="checkbox"
64 checked={remember}
65 onChange={handleRememberChange}
66 />
67 Remember me?
68 </label>
69
70 <button type="submit" data-testid="submit">
71 Sign in
72 </button>
73 </form>
74 );
75}
76
77export default LoginForm;
This is a simple login form containing a username, password, and checkbox. It uses the useState
hook to maintain internal state, as well as a shouldRemember
prop to set the default state of the checkbox.
Create a new test file, src/__tests__/LoginForm.test.tsx
:
1import React from "react";
2import { render, fireEvent, waitForElement } from "@testing-library/react";
3
4import LoginForm, { Props } from "../LoginForm";
5
6describe("<LoginForm />", () => {
7 test("should display a blank login form, with remember me checked by default", async () => {
8 // ???
9 });
10});
This is the skeleton of our test suite. We start by importing the utilities needed from @testing-library/react
:
render
helps render components and returns find helper methods.fireEvent
is for simulating events on DOM elements.waitForElement
is useful when waiting for UI changes to occur.We've defined a test but it has no implementation. To run the test suite, start the test runner:
1npm run test:watch
This will watch for changes as we implement the tests. Since there's no assertions, the first test passes:
Remember, we want to test important behavior, so the first test will ensure that we are rendering the username, password, and "remember me" checkbox for a blank form.
One useful tip is to encapsulate rendering the component you are testing using a render helper function so that you can handle props overriding and make your tests more maintainable:
1function renderLoginForm(props: Partial<Props> = {}) {
2 const defaultProps: Props = {
3 onPasswordChange() {
4 return;
5 },
6 onRememberChange() {
7 return;
8 },
9 onUsernameChange() {
10 return;
11 },
12 onSubmit() {
13 return;
14 },
15 shouldRemember: true
16 };
17 return render(<LoginForm {...defaultProps} {...props} />);
18}
Here, we are leveraging TypeScript to ensure our props are consistently applied to the LoginForm
component. We start by defining some "default" props and then spreading additional props passed into the function as overrides. The override props are typed as Partial<Props>
since they are optional.
If the Props
interface changes, TypeScript will throw a compiler error and the test helper will need to be updated, ensuring our tests are kept updated.
React Testing Library offers a quick way to find elements using helpers.
We will use findByTestId
to find elements by their data-testid
attribute value. You can also use getByTestId
which is the sync version. We've given the <form>
element a test ID value of login-form
which we can query.
Testing library provides additional Jest matchers through @testing-library/jest-dom. In our example, we are using semantic form markup using the <form>
element and input name
attributes so we can use the toHaveFormValues
matcher to more easily assert if the form values are what we expect:
1test("should display a blank login form, with remember me checked by default", async () => {
2 const { findByTestId } = renderLoginForm();
3
4 const loginForm = await findByTestId("login-form");
5
6 expect(loginForm).toHaveFormValues({
7 username: "",
8 password: "",
9 remember: true
10 });
11});
The test still passes in the Jest output:
The next tests should be to ensure that the user can interact with our form in the expected ways:
We'll need to use the fireEvent
utility to help us modify the form's state. Rather than trying to test the internal state of the form, we've introduced callbacks on the props that we can mock to ensure that we get the value we expect for each interaction. Not only is this useful for testing, it is useful for any consumers of the LoginForm
component.
Start by entering a username and password, and ensuring we get a change event back with the values we expect:
1test("should allow entering a username", async () => {
2 const onUsernameChange = jest.fn();
3 const { findByTestId } = renderLoginForm({ onUsernameChange });
4 const username = await findByTestId("username");
5
6 fireEvent.change(username, { target: { value: "test" } });
7
8 expect(onUsernameChange).toHaveBeenCalledWith("test");
9});
10
11test("should allow entering a password", async () => {
12 const onPasswordChange = jest.fn();
13 const { findByTestId } = renderLoginForm({ onPasswordChange });
14 const username = await findByTestId("password");
15
16 fireEvent.change(username, { target: { value: "password" } });
17
18 expect(onPasswordChange).toHaveBeenCalledWith("password");
19});
Using fireEvent.change
we can simulate a form change event on the inputs and assert that the right value was passed to the prop callback. We locate the inputs using their respective data-testid
values of username
and password
.
Now, let's make sure the user can check the "remember me" checkbox:
1test("should allow toggling remember me", async () => {
2 const onRememberChange = jest.fn();
3 const { findByTestId } = renderLoginForm({
4 onRememberChange,
5 shouldRemember: false
6 });
7 const remember = await findByTestId("remember");
8
9 fireEvent.click(remember);
10
11 expect(onRememberChange).toHaveBeenCalledWith(true);
12
13 fireEvent.click(remember);
14
15 expect(onRememberChange).toHaveBeenCalledWith(false);
16});
This time we're using fireEvent.click
to toggle the checkbox on and off. By passing the initial prop shouldRemember: false
we know, after the first click, that the value should be true
.
Finally, let's fill in all the form fields and submit the form and ensure we receive the expected values:
1test("should submit the form with username, password, and remember", async () => {
2 const onSubmit = jest.fn();
3 const { findByTestId } = renderLoginForm({
4 onSubmit,
5 shouldRemember: false
6 });
7 const username = await findByTestId("username");
8 const password = await findByTestId("password");
9 const remember = await findByTestId("remember");
10 const submit = await findByTestId("submit");
11
12 fireEvent.change(username, { target: { value: "test" } });
13 fireEvent.change(password, { target: { value: "password" } });
14 fireEvent.click(remember);
15 fireEvent.click(submit);
16
17 expect(onSubmit).toHaveBeenCalledWith("test", "password", true);
18});
The form is fully tested and meets our expectations. Using the React Testing Library encouraged us to write tests without relying on the internal state and use the DOM directly, just as our users would.
As you can see above, testing React hooks like useState
doesn't require anything special during tests when used in components. In general, you do not need to test hooks in isolation unless they are complex or you are building a reusable library.
If you do need to test hooks you can leverage react-hooks-testing-library. This makes it easy to test hooks without requiring a component to wrap them.
There is no extra work necessary for writing the tests in TypeScript, you can follow the documentation for guidance on usage.
Testing React components with TypeScript is not too different from testing with JavaScript. In this guide we set up the Jest and React Testing Library with TypeScript support. We also learned the basics of testing a React component’s state using data-testid
attributes and callbacks by firing events.
The source code for this guide is available for reference on GitHub.
Explore these React and Typescript courses from Pluralsight to continue learning: