loading...
Cover image for Make testable components using the Humble Object pattern

Make testable components using the Humble Object pattern

krofdrakula profile image Klemen Slavič Updated on ・9 min read

Cover image courtesy of Chris McFarland.

If you've been around React or any of its siblings (Preact and Inferno), you've probably hit a concrete wall trying to figure out how to test the behaviour of components.

You might be thinking, "it's OK, I can just throw Jest, JSDOM and Enzyme at the problem, or just run tests in a real browsers with Puppeteer or Karma!"

And if you're not getting nervous about setting all these tools up for use in a CI environment, chances are you haven't dealt with issues surrounding the setup of these tools, especially if you happen to stray a little off the beaten path.

If you're like me, you want your tests to be fast and to ideally run without an HTML page.

If you're reading this article to see how to set up a browser test for your components, you'll be bitterly disappointed. The tests I write here can all run vanilla node with no server-side DOM fakery!

On the bright side, though, you will hopefully discover a different way to separate the testable bits of components without having to spin up a fake (or real) browser!

Pull up a chair, grab your favourite hot beverage and let's talk about...

...the Humble Object pattern

In Uncle Bob's book Clean Architecture, our dear old Uncle talks about making the painful parts of our application do the least amount of work and concentrate the bulk of our important logic in separate, agnostic code.

He talks about the Humble Object, something so simple and straightforward as to be almost transparent, to the point that it would seem silly to test. This is what our View object should look like. It's the part that's painful to test, and we want it to be as simple as possible.

On the other side, we have the Presenter, which feeds data to the Humble Object and takes care of any heavy lifting that needs to be done in order to render the View.

OK, that sounds reasonable, but the how is still a bit hazy. Let's be a bit more specific.

Say you're writing a component that should render a list of items (with links), described by the following sample data:

const PHOTO_LIBRARY = [
  { id: 1, url: '...', title: '...', description: '...' },
  // ...
];

That sounds straightforward enough. Let's code it up, PFC style! In case you're not familiar with the acronym, PFC stands for Pure Functional Component, also known as a Stateless Component. Really, any function that takes props and returns JSX is considered a Stateless Component or PFC.

/**@jsx h*/
import { h } from 'preact';            // cuz that's how I roll these days
import styles from './photo_list.css'; // I'm a sucker for CSS modules

const PhotoList = ({ photos = [] }) => (
  <ul className={styles.list}>
    {photos.map(photo => (
      <li className={styles.item}>
        <a href={photo.url}>
          <img src={photo.url} alt={photo.description}/>
        </a>
      </li>
    ))}
  </ul>
);

export default PhotoList;

It works, but that nesting should ideally make you cringe a bit. So far, this doesn't prove too painful to parse, but it might be useful to extract the item into its own component.

export const Photo = ({ url, description, title }) => (
  <li className={styles.item}>
    <a href={url}>
      <img src={url} alt={description} title={title} />
    </a>
  </li>
);

const PhotoList = ({ photos = [] }) => (
  <ul className={styles.list}>
    {photos.map(Photo)}
  </ul>
);

So far, even at a glance, we're quite confident this code should work. Let's kick things up a notch and display a message when the list is empty.

const PhotoList = ({ photos = [] }) => {
  if (photos.length == 0) {
    return (
      <div className={styles.empty}>🤔 Wut.</div>
    );
  } else {
    return (
      <ul className={styles.list}>
        {photos.map(Photo)}
      </ul>
    );
  }
};

At this point, a slight twitch might develop in the upper region of your right cheek (or is that just me?). You could turn that expression into a ternary to get rid of the curly braces, but that just obfuscates the more obvious if statement that's really at the core of the problem.

While it may seem that I'm overthinking something so obvious, consider how you or any other developer would approach such a component in the future. Wouldn't it be easier to just add another else if extension, or just nest an expression? How many nestings would you allow? When is enough really enough?

Not to mention I haven't even considered writing a test yet!

Enter Presenter, stage left

Let's break down the top component into two parts: one that decides which view to render and the data each needs, and the rest that are just dumb components.

Also, now is a good time to show how a higher-order component (commonly abbreaviated as HOC) can really help to make our lives easier. Let's write a HOC to connect a presenter with a component.

const DEFAULT_PRESENTER = props => props;

export const withPresenter = (Inner, presenter = DEFAULT_PRESENTER) =>
  (props, context) =>
    <Inner {...presenter(props, context)} />;

Let's break down what withPresenter does:

  1. It takes a component and a presenter function, and returns a pure functional component (a function that returns JSX).
  2. This component passes its props and context to the presenter function, which returns a new set of props.
  3. The Inner component is rendered using the props returned from the presenter.

The default presenter just returns props as is, so wrapping a component without a presenter does nothing. Let's use this to extract the logic of processing props into its own function.

export const PhotoList = ({ photos }) => (
  <ul className={styles.list}>
    {photos.map(Photo)}
  </ul>
);

export const EmptyList = () => (
  <div className={styles.empty}>🤔 Wut.</div>
);

export const PhotoListTemplate = ({ photos, isListEmpty, Empty, List }) => (
  isListEmpty ? <Empty/> : <List photos={photos} />
);

export const photoListPresenter = ({ photos = [] }) => ({
  photos,
  isListEmpty: photos.length == 0,
  Empty: EmptyList,
  List: PhotoList
});

export default withPresenter(PhotoListTemplate, photoListPresenter);

First off, you'll notice that we export a heck of a lot of components for this little example, and with good reason.

UI changes a lot. You don't want to slow yourself down by asserting things like explicit style assertions, or text matches against strings in HTML. You want to avoid testing something that's mode du jour at the mercy of your design team.

Now, I'm not saying you should just write your components and deploy. I would highly recommend you publish a live styleguide up-to-date with the latest code, using something like Storybook to showcase all the various parts of the UI. Make it publish as part of every commit. Make it accessible to everyone on the team, especially the ones who proposed the changes so they can verify the look and feel of the components themselves.

And, let's face it, 100% test coverage is a pipe dream whenever the UI is involved. Some eyeball testing cannot be avoided.

So let's talk about the withPresenter bit.

We created a presenter that returns the following props:

  1. photos: an array of the original data
  2. isListEmpty: a boolean that does what it says
  3. Empty: a component to be rendered when the list is empty
  4. List: a component to be rendered when there are photos to display

This is now bound to the PhotoListTemplate, which then renders either Empty or List, depending on the isListEmpty flag.

We can now render each of the components unconditionally with different prop combinations without worrying about any logic!

Well, almost. There's still that ternary expression in PhotoListTemplate.

Level up: makeChoice()

Here's a nifty way to get rid of if-else constructs in pure functional components:

export const makeChoice = (predicate, True, False) =>
  (props, context) =>
    predicate(props, context) ? <True {...props}/>: <False {...props}/>;

Can you guess what this does?

Yup. You guessed it. If predicate returns true when passed props, it will return whatever True returns, and vice versa.

Let's rewrite our template component with this in mind:

export const PhotoListTemplate = makeChoice(
  props => props.isEmptyList,
  ({ Empty }) => <Empty/>,
  ({ List, photos }) => <List photos={photos} />
);

That might look a bit odd, but let's address what the three arguments to makeChoice are:

  1. predicate is a function that returns true when isEmptyList from props is true.
  2. When true, we take the Empty component from props and render it.
  3. When false, we render List by passing photos to it.

Tah-dah! You've now successfully removed any and all logic from your presentation components. All of your view components are completely declarative with no logic.

Now let's look at how to test our presenter and template.

Testing the presenter and template

Since presenter is just a function that takes props and returns props, we can create a couple of tests for it:

// we're using Jest with Jasmine syntax here, but feel free
// to use any test framework you like, or just use `console.assert`!

import { expect } from 'chai'; // kill your darlings!

import { photoListPresenter } from './photo_list';

describe(photoListPresenter, () => {

  it('should correctly determine an empty list', () => {
    const photos = [];

    expect(photoListPresenter({ photos }))
      .to.have.property('isEmptyList', true);
  });

  it('should correctly determine a populated list', () => {
    const photos = [{ id: 1 }];

    expect(photoListPresenter({ photos }))
      .to.have.property('isEmptyList', false);
  });

});

Let's also add tests for the template function, which we have to render using our view library (in this case, using preact-render-to-string):

/**@jsx h */
// this render function doesn't require a DOM
import render from 'preact-render-to-string';
import { h } from 'preact';
import { expect } from 'chai';

import { PhotoListTemplate} from './photo_list';

describe(PhotoListTemplate, () => {

  it('should render an empty list when there are no photos to show', () => {
    const photos = [];
    const Empty = jest.fn(() => null); // we make a fake component to see
    const List = jest.fn(() => null);  // which one of these is rendered

    render(
      <PhotoListTemplate
        photos={photos}
        isEmptyList={true}
        Empty={Empty}
        List={List}
      />
    );

    expect(Empty.mock.calls.length).to.equal(1); // was Empty rendered?
    expect(List.mock.calls.length).to.equal(0); // was List not rendered?
  });

  it('should render a populated list when there are photos to show', () => {
    const photos = [{ id: 1 }];
    const Empty = jest.fn(() => null);
    const List = jest.fn(() => null);
    render(
      <PhotoListTemplate
        photos={photos}
        isEmptyList={false}
        Empty={Empty}
        List={List}
      />
    );

    expect(Empty.mock.calls.length).to.equal(0); // was Empty not rendered?
    expect(List.mock.calls.length).to.equal(1); // was List rendered?
    expect(List.mock.calls[0][0]).to.eql({ photos }); // was List given photos?
  });

});

This test pretty much closes the loop on any logic previously entagled within the rendering code. You can, of course, also test to see if my implementation of withPresenter and makeChoice actually work, which completes the coverage of all of the logical bits of your components.

Test resilience

So what if we decide to change the photos prop from an array to a Map using ids as keys and the rest as the value? Which parts of the code have to change to adapt?

const PHOTOS = new Map([
  [1, { url: '...', title: '...', description: '...' }]
});

We know the presenter will be our first point of contact, so let's make sure to pass the correct data to our components:

export const photoListPresenter = ({ photos = new Map() }) => ({
  photos: Array.from(photos.entries()).map(([id, photo]) => ({ id, ...photo })),
  isListEmpty: photos.size > 0,
  Empty: EmptyList,
  List: PhotoList
});

We also have to fix our tests to use pass Map instead of an array. Fortunately for us, we only need to change the presenter test, since the rest of the component tree is unaffected by the change.

describe(photoListPresenter, () => {

  it('should correctly determine an empty list', () => {
    const photos = new Map();

    expect(photoListPresenter({ photos }))
      .to.have.property('isEmptyList', true);

    expect(photoListPresenter({ photos }).photos)
      .to.eql([]); // is photos an array?
  });

  it('should correctly determine a populated list', () => {
    const photos = new Map([
      [1, { title: 'hello' }]
    ]);

    expect(photoListPresenter({ photos }))
      .to.have.property('isEmptyList', false);

    expect(photoListPresenter({ photos }).photos)
      .to.eql([{ id: 1, title: 'hello' }]); // is photos an array with id?
  });

});

If you now run the test suite, all tests pass, including the previously written template test.

What about if the EmptyList component changes, or when you decide that PhotoList should render its list into a <canvas> instead? What if we also need to show a spinner while photos are still being loaded as part of an AJAX call? What about if the photos also have Dates associated that need to be formatted in the user's locale?

Since we now have a function where all the logic lives, it becomes easier for anyone approaching this code to add things without affecting an entire subtree of components. No need for strict policies or complex linting rules, save for one: put logic into the presenter, and the rest into a template.

Conclusion

Having used this pattern in production, I find that presenters provide a great way to define a boundary where all the potentially messy bits live. It doesn't require a rendering library to check what the output is, it just deals with data. It doesn't care if you use React, Preact, Inferno, Vue or any other component-based library. The presenter tests run just fine in any JS environment and test framework without needing to fake any DOM or spawn any browser.

This doesn't prevent you from building browser tests for the rest of the presentation components, however. You can still build and run tests for all the rest of the components, but by removing logic from them, you've cut down the possible combination space you'd have to test to ensure comprehensive coverage.

In our own Webpack, Babel, TypeScript and Preact-infused project, we run these tests on every prepush git hook, which means that the tests run every time you attempt to push to GitHub. If those tests required a minute to run, you can see how that would make people want to sidestep that suite.

This way, since the tests run within 3 seconds of when you hit Enter, no one complains, and you are prevented from pushing until you fix the tests, which hopefully only boils down to changing the Presenter ones.

Peace out!

Posted on by:

krofdrakula profile

Klemen Slavič

@krofdrakula

My dev career is of legal drinking age.

Discussion

markdown guide