Testing UI Components Part 1: Atoms + Molecules

By Khalil Bouaazzi on 3/23/2021

Testing: The final frontier (or in TDD, the initial frontier). There are a ton of ways to do it but in general, it’s hard to know what to test. With all the different testing frameworks out there, it’s also pretty hard to choose how to test. At OrderMyGear, when it comes to unit testing, we wanted the ability to test using something that would work for our internal UI framework library as well as our micro-ui server-side rendered apps.

Here at OMG, our SSR apps are a blend of Node/Express backends with React frontends written in Typescript. We also have a dedicated component library that needs to be tested and we wanted our selection to work with all the above. We settled with Jest being our test runner as it is customizable, easy to use, and very well documented. As far as the frontend, our big decision was to choose between React Testing Library and Enzyme.

This post is the first in a multi part series where I talk about using React Testing Library, Emotion, and Typescript. This will talk about ways to classify components and test simple components. Future parts dive into more complex components with internal state, reducers, and asynchronous behavior.

 

Best Practices: Atomic Design

Before jumping into testing components, I think it’s important to discuss what kinds of components need to be tested. What to test generally has to do with how complex a component is. A good way to analyze how complex a component might be would be to try to classify the component using Atomic design principles.

Atomic design is a concept that helps create a mental model of all the things that we need to do to develop a new user interface. It adds structure and builds a loose schedule of things that need to be done. This article isn’t necessarily about atomic design but thinking of a user interface in terms of atoms, molecules, organisms, templates and pages will help us when writing testing.

Atomic design is composed of five distinct categories:

  • Atoms – foundational components that are incapable of being broken down further. Some examples of these components are buttons, inputs, and text.
  • Molecules – components built out of two or more atoms. An example of a molecule is a search input: An input, a label, and a button. A more complex molecule might be a typeahead, which includes a dropdown
  • Organisms – components built from Molecules. An example of an organism might be a site header or list/grid of card elements.
  • Templates – Molecule and organisms that have been put into a structure and organized into an underlying layout
  • Pages – Once a template undertakes a specific implementation of content, it becomes a page. Pages refer to templates that have been populated by data

While this article is not meant to be about atomic design, it is important to understand what kind of components require what levels of testing. Oftentimes, atoms and molecules have interactions that are defined by the organisms that contain them and thus, limits what is truly testable. This article is about how to write effective tests given the scope of a particular component.

 

Testing Atoms

With simple components, it is important to understand all of the states a component can be in and what interactions can trigger these states. To demonstrate this, consider an atom <Button /> component. A <Button /> can be hovered over and it can be clicked as well as it’s default, neutral state. When clicked, it should do something.

Using Jest and the React Testing Library, we can easily test a typical atom component. Note that atom and smaller molecule components can include a snapshot test. This sort of test is useful for components like those that will live in the ui-framework. As soon as they’re built and are adopted by other repos, they should require very few changes. If a change is necessary, we can delete the previous snapshot in lieu of an updated one. Below is a good example of a simple atom component:


// Button.tsx
import React, { FunctionComponent } from ""react"";
import styled from ""@emotion/styled"";

interface ButtonProps {
 onClick: () => void;
}

const StyledButton = styled.button`
 background-color: red;
 &:hover {
   background-color: orange;
}
&:active {
   background-color: blue;
}
`;

const Button: React.FC<ButtonProps> = (props) => {
 const { onClick } = props;
 return <StyledButton onClick={onClick}>{props.children}</StyledButton>;
};

export default Button;

 

In this case, we have a button styled using emotion.js. It accepts one handler function prop that is called when the button is clicked. Now let’s look at a few jest tests for this button, which we choose to place in the same directory as the component itself:

// Button.test.tsx (Note that tests are colocated with components)
import React from ""react"";
import { render, fireEvent } from ""../test-utils"";
import Button from ""./"";
describe(“”<Button />””, () => {
test(“”it should render a button””, async () => {
const mockClick = jest.fn();
const { getByText } = render(
<Button onClick={mockClick}>NiceButton</Button>
);
const TestButton = getByText(“”NiceButton””);
expect(TestButton).toBeInTheDocument();
expect(TestButton).toMatchSnapshot();
});

test(“”it should perform the passed onClick function””, async () => {
const mockClick = jest.fn();
const { getByText } = render(
<Button onClick={mockClick}>NiceButton</Button>
);
fireEvent.click(getByText(“”NiceButton””));
expect(mockClick.mock.calls.length).toBe(1);
});
});

 

Our first test is a snapshot test: given a button, can we verify that it consistently renders in the same way every time? As mentioned previously, this kind of test is particularly useful for components within a shared library that doesn’t change often. The line expect(TestButton).toMatchSnapshot(); will generate a snapshot file that is then compared against all future runs of this test (including on your CI server). Our second test verifies that the behavior of the button is what we expect. Given a button, we pass down a jest mock function that can report back to the test how often it was called. You can see how the last line of our example verifies that it was called exactly one time if someone clicks on it.

Testing Molecules

Now that we have a <Button /> we can create a <ButtonGroup /> molecule component. As molecules begin to grow and change, they may not be as resilient to snapshot testing. This can be handled on a case by case basis but by default, we should include a snapshot everywhere we can. In this example, we can pass two props: a “children” prop that allows us to pass any number of Buttons, and a “direction” prop that allows us to specify a vertical or horizontal orientation.

// ButtonGroup.tsx
import React, { FunctionComponent } from ""react"";
import styled from ""@emotion/styled"";

interface ButtonGroupProps {
 direction: ""row"" | ""column"";
 children?: React.ReactNode;
}

const VerticalGroup = styled(""div"")`
 display: flex;
 flex-direction: column;
`;

const HorizontalGroup = styled(""div"")`
 display: flex;
 flex-direction: row;
`;

const ButtonGroup = (props: ButtonGroupProps) => {
 const { direction, children } = props;
 return direction === ""column"" ? (
  <VerticalGroup role=""group"">{children}</VerticalGroup>
) : (
  <HorizontalGroup role=""group"">{children}</HorizontalGroup>
);
};

export default ButtonGroup;

Now that we have a functional ButtonGroup, let’s write some tests for it:

import ButtonGroup from ""./"";
import Button from ""../Button"";

describe("<ButtonGroup />", () => {
 const click = () => {};
 test(""it should render a column of buttons"", async () => {
  const { getByRole, getAllByRole } = render(
   <ButtonGroup direction=""column"">
    <Button onClick={click}>1</Button>
    <Button onClick={click}>2</Button>
    <Button onClick={click}>3</Button>
   </ButtonGroup>
  );
  expect(getByRole(""group"")).toBeInTheDocument();
  expect(getAllByRole(""button"").length).toBe(3);
  expect(getByRole(""group"")).toHaveStyle(""flex-direction: column"");
  expect(getByRole(""group"")).toMatchSnapshot();
 });

 test(""it should conditionally load a row of buttons"", async () => {
  const { getByRole } = render(
   <ButtonGroup direction=""row"">
    <Button onClick={click}>1</Button>
    <Button onClick={click}>2</Button>
    <Button onClick={click}>3</Button>
   </ButtonGroup>
  );
  expect(getByRole(""group"")).toBeInTheDocument();
  expect(getAllByRole(""button"").length).toBe(3);
  expect(getByRole(""group"")).toHaveStyle(""flex-direction: row"");
  expect(getByRole(""group"")).toMatchSnapshot();
 });
});

By now, you should have a decent grasp on how to test atom and molecule-sized components. Check out the next post for more advanced component testing!

 

Khalil Bouaazzi – OMG’er #114
Senior Software Engineer
Connect with me on LinkedIn

 

About OrderMyGear

OrderMyGear is an industry-leading sales tool, empowering dealers, distributors, decorators, and brands to create custom online pop-up stores to sell branded products and apparel. Since 2008, OMG has been on a mission to simplify the process of selling customized merchandise to groups and improve the ordering experience. With easy-to-use tools, comprehensive reporting, and unmatched support, the OMG platform powers online stores for over 3,000 clients generating more than $1 billion in online sales. Learn more at www.ordermygear.com.

Media Contact: Lauren Seip | lauren.seip@ordermygear.com | 281-756-7915