Part III: Testing strategy and testable components
- The many ways to test React code
- A good testing strategy example
- Make your components testable
Tests are not ubiquitous in React applications, maybe because of misconceptions around testing.
When talking about testing front-end apps, people raise the “end-to-end” test flag, which is expensive to write and maintain.
However, React applications can be tested in many useful and different ways.
1. The many ways to test React code
The drawing below shows four different ways to test React applications, from the most complex, complete, and expensive to the simplest, isolated, and fastest.
Top-bottom: from the most complex, complete and expensive to simplest, isolated and fastest
End-to-end (E2E) tests consist of testing the application like a user.
Such tests require running the application to make assertion after interactions.
Libraries like
Cypress or
Nightwatch help by handling all the complexity of simulating the browser that runs the tests by providing simple APIs.
Like quoted earlier, the
E2E tests are expensive to write and maintain since most of them are based on CSS selectors.
Another approach is Integration tests.
The integration test consists of testing interactions between components.
Those tests don’t require running the application by simulating a browser since a library like
Enzyme helps by providing API over a virtual DOM —
“Shallow rendering.”Enzyme can also be used to do Unit tests, which consist of testing components in an isolated way — useful to test components with complex rendering rules.
A last interesting testing strategy is Business logic unit tests.
Most components rely on some logic — pure functions, specific to your application/product, which is called Business logic.
This Business logic is — most of the time — the initiator of the rendering rules of your component.
This means that only testing this Business logic is good enough — versus testing the whole component.
Doing Business logic unit tests can be achieved even lighter tests using Jest — since business logic is pure JavaScript (without React).
(We will see an example of a Business logic test later in this section.)
We now have a clear vision of all the possible ways to test React applications, let’s see a testing strategy example.
2. A good testing strategy example
The following testing strategy is an example of how I would test a medium-sized production React application (400–500 components) in a team of 3 engineers.
1. Test critical business logic
2. Add test when fixing regression
3. E2E tests for critical paths
Given the small size of the team for the project, we cannot afford to test all components; however, having no tests at all would make the app fragile.
In order to lower the cost of testing, we will write test in the critical part of our apps (on Business logic), also when fixing bugs (to avoid regression).
As a bonus, some E2E tests on the critical paths of our application — this is called
smoke tests.
What is a “critical path”?
- Log in to the application
- Ability to create a task to delegate to my assistant
- Ability to update my billing information
What is NOT a “critical path”?
- Ability to upload an image in the chat
- The tasks snooze feature
A critical path is the set of application’s actions that are the most valuable for your product.
3. Make your components testable
We now have a testing strategy; however, we have to make sure that our code is ready to be tested.
There are many obstacles when it comes to testing React components; let’s see an example.
Our Whatsup chat example application now has a new “Catch-up” feature (shown below).
1import * as React from "react";
2import { reduce } from "lodash";
3
4import { useGetCurrentUser } from "./getCurrentUser";
5import { useGetEvents } from "./getEvents";
6
7import "./style.css";
8
9const userPropToHuman: { [k: string]: string } = {
10 avatar_name: "choose an avatar",
11 birthdate: "enter your birthdate",
12 timezone: "fill your current timezone"
13};
14
15const Headups: React.SFC = () => {
16 const me = useGetCurrentUser();
17 const events = useGetEvents();
18
19 const hasNotifications = Notification.permission === "granted";
20
21 const incompleteProfile: string[] = me
22 ? reduce<any, any>(
23 me,
24 (missing, value, prop) => {
25 if (!value) {
26 missing.push(prop);
27 }
28 return missing;
29 },
30 []
31 )
32 : [];
33
34 return incompleteProfile.length ? (
35 <div className={"headsupOuter"}>
36 {`Your profile is ${100 - incompleteProfile.length * 20}% complete!`}{" "}
37 <br />
38 {`Please take 2min to fill the following info: ${incompleteProfile
39 .map(p => userPropToHuman[p])
40 .join(", ")}`}
41 </div>
42 ) : events && events.length ? (
43 <div className={"headsupOuter"}>
44 <ul>
45 {events.map(event => (
46 <li>{event.event}</li>
47 ))}
48 </ul>
49 </div>
50 ) : !hasNotifications ? (
51 <div className={"headsupOuter"}>
52 {
53 "Enabling notifications will keep you informed of new messages and events"
54 }
55 </div>
56 ) : null;
57};
58
59export default Headups;
<Headups /> component’s code
We want to add some test after a small regression on the “profile progression” code — now fixed.
Testing it will be required to do a shadow rendering unit test of the component since the logic of “profile progression” is wrapped in the component.
Let’s refactor our component in order to allow the business logic unit tests that will be easier to implement and maintain.
utils.tsx
1import { reduce } from "lodash";
2import { User } from "../Headsup/getCurrentUser";
3
4const userPropToHuman: { [k: string]: string } = {
5 avatar_name: "choose an avatar",
6 birthdate: "enter your birthdate",
7 timezone: "fill your current timezone"
8};
9
10export interface ProfileCompletion {
11 percentage: number;
12 missingInfos: string[];
13}
14
15export const profileCompletion = (me: User): null | ProfileCompletion => {
16 const incompleteProfile: string[] = me
17 ? reduce<any, any>(
18 me,
19 (missing, value, prop) => {
20 if (!value) {
21 missing.push(prop);
22 }
23 return missing;
24 },
25 []
26 )
27 : [];
28
29 return incompleteProfile.length > 0
30 ? {
31 percentage: 100 - incompleteProfile.length * 20,
32 missingInfos: incompleteProfile.map(p => userPropToHuman[p])
33 }
34 : null;
35};
Headsup.tsx (refactored)
1import * as React from "react";
2
3import { useGetCurrentUser } from "../Headsup/getCurrentUser";
4import { useGetEvents } from "../Headsup/getEvents";
5import { profileCompletion } from "./utils";
6
7import "../Headsup/style.css";
8
9export interface Props {
10 hasNotifications: boolean;
11}
12
13const Headups: React.SFC<Props> = ({ hasNotifications }) => {
14 const me = useGetCurrentUser();
15 const events = useGetEvents();
16
17 const completion = profileCompletion(me);
18
19 return completion ? (
20 <div className={"headsupOuter"}>
21 {`Your profile is ${completion.percentage}% complete!`} <br />
22 {`Please take 2min to fill the following info: ${completion.missingInfos.join(
23 ", "
24 )}`}
25 </div>
26 ) : events && events.length ? (
27 <div className={"headsupOuter"}>
28 <ul>
29 {events.map(event => (
30 <li>{event.event}</li>
31 ))}
32 </ul>
33 </div>
34 ) : !hasNotifications ? (
35 <div className={"headsupOuter"}>
36 {
37 "Enabling notifications will keep you informed of new messages and events"
38 }
39 </div>
40 ) : null;
41};
42
43export default Headups;
<Headups /> is now using a profileCompletion() helper to derive the state and compute the profile completion.
We now can add a test on the regression in an isolated way:
1import { User } from "../Headsup/getCurrentUser";
2import { profileCompletion } from "./utils";
3
4describe("profileCompletion", () => {
5 describe("when all infos are missing", () => {
6 const user: User = {
7 first_name: "Charly",
8 last_name: "POLY",
9 avatar_name: null,
10 birthdate: null,
11 timezone: null
12 };
13
14 const completion = profileCompletion(user);
15
16 it("should return a percentage of 40%", () => {
17 expect(completion.percentage).toEqual(40);
18 });
19
20 it("should return proper missingInfos", () => {
21 expect(completion.missingInfos).toEqual([
22 "choose an avatar",
23 "enter your birthdate",
24 "fill your current timezone"
25 ]);
26 });
27 });
28});
The same idea could apply if we wanted to test the whole <Headups /> component.
Even with the refactored version, we will face two issues to implement shadow rendering unit tests.
1// [...]
2
3
4const Headups: React.SFC<Props> = ({ hasNotifications }) => {
5 const me = useGetCurrentUser();
6 const events = useGetEvents();
7
8 const completion = profileCompletion(me);
9
10 return completion ? (
11
12 // [...]
13}
me and events rely on custom hooks to load the data, and hasNotification on a browser API — in the global scope.
In order to test <Headups />, we will have to mock useCurrentUser(), useGetEvents(), and Notification global object.
This is totally doable by using
jest.mock, however, it will make our test fragile, breaking every-time the signature of those custom hooks or global object changes.
If testing the rendering logic of <Headups /> is the most important, then another refactoring could help us:
1// [...]
2
3export interface Props {
4 hasNotifications: boolean;
5 events: Event[];
6 me: User[];
7}
8
9
10const Headups: React.SFC<Props> = ({ me, events, hasNotifications }) => {
11 const completion = profileCompletion(me);
12
13 return completion ? (
14
15 // [...]
16}
Here, our unit test will just have to pass “test data” to our <Headups /> component (in the application, <Headups /> will receive data from a Container component).
In that way, we make our test stronger and easier to maintain.
Conclusion
- React applications can be tested in many useful and different ways: E2E tests, Integrations tests, Unit shadow rendering tests, Unit business logic tests
- Invest time in testing the most important part of your product by defining a testing strategy
- Make sure your component can be tested easily by avoiding coupling and components embedded in business logic.
End of our journey, the beginning of yours
Maintainability is all about finding ways to keep your application in movement.
You will find in the introductory article the summary of all the tips and principles of this series; it is now your time to choose which ones are a priority for your business and build your architecture principles!