Confident React App – Part 5

austin powers - Oh, Behave!

Here we go again! The “Confident React App” series of posts is finally back after a long period away. You can follow from the start.

From the feature specs – defined in the 1st post of the series – we can observe some important information regarding interactions:

  1. list item selection is toggled when clicked.
  2. the list group works with a single selection of list items.

The second point was not explicit from the specs (and it was intentional so that you could comment on it. Did you notice it?) but we discovered it after some (imaginary) team discussion (backlog grooming, pre-iteration meeting, etc). The team must extensively communicate verbally and written in preparation for the implementation. Many questions arise when the code gets written, especially in the early stages of development. Effective communication is key!

With that in the clear, it’s time to start coding the user interaction of our app.

Oh, behave…

We’re introducing a new component which whole’s responsibility is to handle user interactions and state management: <SingleSelectionListGroup>.

We’ve “transformed” the component’s specification into tests:

// src/components/listGroup.spec.js
// @flow
describe("<SingleSelectionListGroup />", () => {
  it('renders according to specification', () => {});

  describe('when an item is clicked', () => {
    describe('and it is NOT selected', () => {
      it('selects the clicked item', () => {});
      it('deselects any other selected item - single selection', () => {});
    });
    
    describe('and it IS selected', () => {
      it('deselects the clicked item', () => {});
    });

    it('calls the given onChange callback with the current selected index and value', () => {});
  });
});

Now it’s the tricky part: implementing the tests in a way that it’s not coupled with the component’s implementation. 

This is important because we want to be able to refactor our code in ways that better suit the implementation needs without having to modify any of its tests. Attention: if the public API changes, that’s not a refactor! Code refactoring is the process of restructuring existing computer code without changing its external behavior.

You may ask yourself, “What is knowing implementation details?”. If the test code looks for an internal id or CSS class, is it an implementation detail? How about looking for a dependent component? Maybe a function from a 3rd party library? If the test knows one private implementation detail, does it mean that it can use any other private implementation detail as well?

To be honest, I don’t have a definitive answer to this dilemma. There are as many answers as there are development teams out there. And there’s no correct answer, only trade-offs. Mine, which gives me confidence in my code, is as follows.

We want to test the functionality of <SingleSelectionListGroup> component and we know that it must render according to Bootstrap’s style, therefore, it will use <ListGroup> and <ListGroupItem>. But the rendering part is just a detail for the new component. It doesn’t care about HTML tags or CSS classes. We just need to test that the render components are used correctly to represent the state and behavior.

Let’s do a quick thought exercise. Imagine that we want to know as little as possible about the rendering details. One way to achieve this is to incorporate the “end-user point of view”. This means writing tests that rely only on the artifacts that end up in the user’s browser/client like text labels and markup. We could test a bunch of features with these artifacts since most of them are possible to know or mock in the test.

There’s a particular caveat in the list item selection case. In order to test it, it is required to look for the active CSS class. But this class is a rendering detail that is the responsibility of the <ListGroupItem> component. Putting it in another way, the tests would know the details of the details.

Some teams argue that “the tests don’t care how active CSS class got rendered as long as it’s there.” And that works too. The main disadvantage is that if <ListGroupItem> changes class name from active to selected, <SingleSelectionListGroupItem> tests would fail for no reason. It’s a change totally unrelated to the component being tested so the tests should not break.

To me, the approach that gives the most confidence is testing that <SingleSelectionListGroup> uses <ListGroup> and <ListGroupItem> correctly, passing the right props. Yes, the test will know some implementation details but it won’t know the details of its internal components. This approach also protects <SingleSelectionListGroup>‘s tests from any changes internal to <ListGroup>and <ListGroupItem>.

More on this topic later. Here’s a test implementation that follows our guidelines. Red:

// src/components/listGroup.spec.js
describe("<SingleSelectionListGroup />", () => {
  it("renders according to specification", () => {
    const wrapper = shallow(
      <SingleSelectionListGroup>
        <ListGroupItem>Item 1</ListGroupItem>
        <ListGroupItem>Item 2</ListGroupItem>
      </SingleSelectionListGroup>
    );

    const listGroup = wrapper.find(ListGroup);
    expect(listGroup).toExist();
    expect(listGroup.find(ListGroupItem)).toHaveLength(2)
  });

  describe("and when an item is clicked", () => {
    describe("and it is NOT selected", () => {
      it("selects the clicked item", () => {
        const wrapper = shallow(
          <SingleSelectionListGroup>
            <ListGroupItem>Item 1</ListGroupItem>
            <ListGroupItem>Item 2</ListGroupItem>
          </SingleSelectionListGroup>
        );

        wrapper.find(ListGroupItem).first().simulate("click");

        expect(
          wrapper.find(ListGroupItem).first()
        ).toHaveProp("active", true);
      });

      it("deselects any other selected item - single selection", () => {
        const wrapper = shallow(
          <SingleSelectionListGroup>
            <ListGroupItem active>Item 1</ListGroupItem>
            <ListGroupItem>Item 2</ListGroupItem>
          </SingleSelectionListGroup>
        );

        expect(
          wrapper.find(ListGroupItem).first()
        ).toHaveProp("active", true);

        wrapper.find(ListGroupItem).last().simulate("click");

        expect(
          wrapper.find(ListGroupItem).first()
        ).toHaveProp("active", false);

        expect(
          wrapper.find(ListGroupItem).last()
        ).toHaveProp("active", true);
      });
    });

    describe("and it IS selected", () => {
      it("deselects the clicked item", () => {
        const wrapper = shallow(
          <SingleSelectionListGroup>
            <ListGroupItem>Item 1</ListGroupItem>
            <ListGroupItem active>Item 2</ListGroupItem>
          </SingleSelectionListGroup>
        );

        expect(
          wrapper.find(ListGroupItem).last()
        ).toHaveProp("active", true);

        wrapper.find(ListGroupItem).last().simulate("click");

        expect(
          wrapper.find(ListGroupItem).last()
        ).toHaveProp("active", false);

        expect(
          wrapper.find(ListGroupItem).first()
        ).toHaveProp("active", false);
      });
    });

    it("calls the given onChange callback with the current selected index and value", () => {
      const changeSpy = jest.fn();
      const wrapper = shallow(
        <SingleSelectionListGroup onChange={changeSpy}>
          <ListGroupItem>Item 1</ListGroupItem>
          <ListGroupItem>Item 2</ListGroupItem>
        </SingleSelectionListGroup>
      );

      wrapper.find(ListGroupItem).first().simulate("click");

      expect(changeSpy).toHaveBeenCalledWith({
        index: 0,
        value: "Item 1",
      });

      wrapper.find(ListGroupItem).first().simulate("click");

      expect(changeSpy).toHaveBeenCalledWith({
        index: -1,
        value: null,
      });
    });
  });
});

Notice how the test above never looks for HTML specific details like tags or CSS classes. Those details are for the render components. The new tests only care about what the component really is supposed to do: handle the selection, user interactions and use the propper render components correctly.

Also notice that the tests only used shallow rendering! That’s the utility tool for isolating the component rendering. This forces the implementation to be very flat which is fine for the current needs. Granted, if the component structure was more complex we would need to use Enzyme’s dive() as well in the tests which are also implementation details.

Now go ahead and make all tests turn green. It’s a pretty cool challenge. You can check my solution on GitHub. Don’t forget to add a new Storybook story with the new component to see it in action.

This is it for today’s entry! I hope the confidence in your code has increased a little bit more with my explanations.

Until next time when, hopefully, this series will end.
Cheers!

Flattr this!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.