15th Sept 2019

Assert Zero Arguments

When testing a function call it is important to check the expected arguments are passed, including functions that have zero arguments.

In the following React component we want to test that clicking the "Exit survey" button calls the function passed to its onExit prop:


const Survey = ({ onExit }) => (
  <button type="button" data-test="exit-survey" onClick={onExit}/>
    Exit survey
  </button/>
)

This interaction can be tested using toBeCalled:


it("To be called", () => {
  const onExitMock = jest.fn().mockName("onExitMock");
  const wrapper = shallow(<Survey onExit={onExitMock} />);

  wrapper.find("[data-test='exit-survey']").simulate('click');

  expect(onExitMock).toBeCalled(); // ← assert function is called
});

// Produces:

 PASS  src/Survey.spec.js
  ✅ To be called

After some user testing the UX team find a higher number of participants exit the survey than hoped. Perhaps personalising the exit message will encourage more people to reconsider.

We update our component to capture the participants name, and subsequently pass it to the onExit prop:


const Survey = ({ onExit }) => {
  const [participantName, setParticipantName] = useState("");

  return (
    <>
      <input
        value={participantName}
        data-test="participant-name"
        onChange={({ target }) => setParticipantName(target.value)} // ← capture participants name
      />
      <button
        type="button"
        data-test="exit-survey"
        onClick={() => onExit(participantName)} // ← pass name to onExit
      >
        Exit survey
      </button>
    </>
  );
};

// Produces:

 PASS  src/Survey.spec.js
  ✅ To be called

Interestingly, but not unexpectedly our test still passes. Technically the assertion is still correct because the onExit prop is still called.

However the test lacks the explicitness required to catch a change in contract between the function and its caller. It lacks awareness of what arguments (if any) the function is called with. What if the parent component of Survey still assumes onExit receives zero arguments?

This is where asserting zero arguments can help. If our original test used toBeCalledWith instead of toBeCalled this change in arity would have been caught:


it("To be called with", () => {
  const onExitMock = jest.fn().mockName("onExitMock");
  const wrapper = shallow(<Survey onExit={onExitMock} />);

  wrapper.find("[data-test='exit-survey']").simulate('click');

  expect(onExitMock).toBeCalledWith(); // ← assert function receives zero arguments
});

// Produces:

 FAIL  src/Survey.spec.js
  ❌ To be called with

  ● To be called with

    expect(onExitMock).toBeCalledWith(...expected)

    Expected: called with 0 arguments
    Received: ""

Now with the test in a failing state it can be amended it to reflect the new functionality. Not only does this improve the accuracy of the test – it also reduces the likeness of a parent component assuming onExit has zero arguments.


it("To be called with the participant name", () => {
  const onExitMock = jest.fn().mockName("onExitMock");
  const wrapper = shallow(<Survey onExit={onExitMock} />);
  const givenParticipantName = "John";

  wrapper
    .find("[data-test='participant-name']")
    .simulate("change", { target: { value: givenParticipantName } });
  wrapper.find("[data-test='exit-survey']").simulate("click");

  expect(onExitMock).toBeCalledWith(givenParticipantName); // ← assert name is passed to onExit
});

// Produces:

 PASS  src/Survey.spec.js
  ✅ To be called with the participant name

Always favour explicitness over in-explicitness when testing function calls!

Never Miss a Post!

If you have enjoyed this blog post I invite you to join my newsletter:

Emails are kept safe with MailChimp And I never ever send spammy emails (Or stay updated with RSS)