React Unit-testing – Finding DOM Elements

Writing unit-tests for your React components are important, but it also important that tests are robust. Brittle tests are not maintainable & cause frustration to developers.

This post addresses once such scenario that can cause brittle unit-tests, which is finding the nodes or DOM elements. Let’s work on making them robust.

DOM elements are identified using selectors such as id or CSS class-names. If the CSS class-names are generated dynamically then using them is out of the question. How using them as your identifier makes your test brittle? Well, if your code is refactored which changes the id of your DOM element or CSS class-name is changed, in such scenario, despite your component having the same functionality, the unit-test(s) will fail. E.g. We were using a <button> element to create a button & id was btnCreate but later on we decided to refactor it & use <a> anchor tag & we refactored id to anchorCreate or lnkCreate.

To make your unit-tests immune to such changes, we use an identifier that is consistent through out the life-cycle of an application. For achieving the robustness we can use data-test attributes for the nodes or DOM Elements we want to identify within the test. An extra attribute we will added to the nodes or DOM Elements we want to find or raise assertions with help of unit testing framework & test utilities in the context & this selector will remain unchanged even if the underlying DOM element is changed which was used to represent the data received or will not be affected by code refactoring (unless the whole element is removed in which case test should fail).

How do we achieve this? Source code is available under a GitHub repository to download here. Let’s discuss about it.

To protect the unit-tests from the changes of id or CSS class-name we will introduce a data-test-id attribute to the components or elements we need to find via unit tests. Refer to the Workout.js component in the code example:

import React from "react";
function Workout({ name, target, group }) {
return (
<div data-test-id="workout-container">
<h4 data-test-id="workout-name">Name: {name}</h4>
<h6 data-test-id="workout-target">Target: {target}</h6>
<h6 data-test-id="workout-group">Group: {group}</h6>
</div>
);
}
export default Workout;
view raw Workout.js hosted with ❤ by GitHub
Workout.js

I’ve added the data-test-id attribute to 4 elements we want to identify for Workout.js component via unit-test. We want to make sure when details about a workout are provided, the container is rendered along with name, target & group. For the example application, we’re using Jest & Enzyme to write this unit test. When the test starts, we setup the component using shallow method of enzyme to render the component. Enzyme returns a ShalloWrapper for the component & within this wrapper we will search & identify the components/elements we’re looking for.

A small utility method is written to search for nodes with data-test-id in shallow rendered component so we don’t duplicate logic across out tests. It is available under test/utils/testUtils.js

/**
* Returns node(s) for the given string attribute value of `data-test-id`
* @function findByTestAttribute
*
* @name
* @param {ShallowWrapper} wrapper – ShallowMounted Enzyme wrapper
* @param {string} attributeValue – value of data-test-id attribute as string
* for searching the nodes
* @returns {ShallowWrapper}
*/
export function findByTestAttribute(wrapper, attributeValue) {
if (!wrapper) {
return null;
}
return wrapper.find(`[data-test-id="${attributeValue}"]`);
}
view raw testUtils.js hosted with ❤ by GitHub
testUtils.js

Now we will write the unit-test using Jest & Enzyme along with the help of above utility method. This is test is now immune from changes of id or CSS class-names. Even if you change the underlying element representing the data the tests run without error. For example to represent the text about workout name one decides to use <span> instead of <h6> the data-test-id can remain same as it represents workout name thus producing robust test. We will look at the unit-test now:

import { shallow } from "enzyme";
import React from "react";
import { findByTestAttribute } from "../test/utils/testUtils";
import workouts from "./data/worksouts.json";
import Workout from "./Workout";
function setup() {
return shallow(<Workout {workouts[0]} />);
}
describe("Workout renders", () => {
test("without error", () => {
const wrapper = setup();
const container = findByTestAttribute(wrapper, "workout-container");
expect(container.exists()).toBe(true);
expect(container.length).toBe(1);
});
test("workout name, target and group", () => {
const wrapper = setup();
const workoutName = findByTestAttribute(wrapper, "workout-name");
expect(workoutName.exists()).toBe(true);
expect(workoutName.text()).toContain("Bench Press");
const workoutTarget = findByTestAttribute(wrapper, "workout-target");
expect(workoutTarget.exists()).toBe(true);
expect(workoutTarget.text()).toContain("Chest");
const workoutGroup = findByTestAttribute(wrapper, "workout-group");
expect(workoutGroup.exists()).toBe(true);
expect(workoutGroup.text()).toContain("Upper Body");
});
});
view raw Workout.test.js hosted with ❤ by GitHub
Workout.test.js

It is clear from the test & code above is that we are adding an extra attribute to the code & this attributes are now part of the code we release to production & any data that is not related to application business logic should be avoided & keep things de-cluttered.

There are two ways to achieve this.

Using a Babel Plug-in:

Eject the react app using npm eject or yarn eject (only if application is created using create-react-app) & use a babel plugin called babel-plugin-jsx-remove-data-test-id. If you’ve created app manually then you can use above mentioned plug-in directly. What this plugin does is, it removes data-test-id attribute from the production build.

README.md is very self-explantory for this plug-in. Also if you name your attribute something different e.g. data-testid or data-test-attr you can let the plug-in know your custom attribute name to be removed from production build.

plugins: [
[ "babel-plugin-jsx-remove-data-test-id",
{ attributes: [
"data-test-id", "selenium-id", "another-attr-to-be-stripped"
]
}
]
];
view raw bableConfig hosted with ❤ by GitHub
babel config to be updated

Without Ejecting from CRA & Adding Babel Plug-in

Well, in this case we will construct a method that will add data-test-id attribute to a component only if the NODE_ENV is set to development.

/**
* Returns and object with `data-test-id` as key and attributeValue as
* associated value to the key
* @function
* @name getDataTestAttribute
*
* @param {string} attributeValue
* @returns {object}
*/
export function getDataTestAttribute(attributeValue) {
return process.env.NODE_ENV !== "production"
? {
"data-test-id": attributeValue,
}
: {};
}
attributeHelper.js

Now we will use the method above to add data-test-id attribute to component/element when the NODE_ENV is development. Look at the updated code of Workout.jsx here. It won’t have data-test-id for the production build.

import React from "react";
import { getDataTestAttribute } from "./helpers/attributeHelper";
function Workout({ name, target, group }) {
return (
<div {getDataTestAttribute("workout-container")}>
<h4 data-test-id="workout-name">Name: {name}</h4>
<h6 data-test-id="workout-target">Target: {target}</h6>
<h6 data-test-id="workout-group">Group: {group}</h6>
</div>
);
}
Workout.js updated with attributeHelper

This strategy can be used with your selenium UI test(s) as well by adding selenium-id to the elements you want to identify & build robust tests. No matter how big or small is your application, this strategy can be used to write unit-test(s) or UI test(s).

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: