React's Shallow Render - Tread Carefully

Before I say anything about shallow render testing with in React, we need
some baseline testing principles.

Tests should exercise public interfaces.

Tests should use functions and objects as they are used in your application.
This means that tests should not directly call private functions and that
that nothing else in the application should access.

Testing should influence design.

Tests are easier to write when modules are focused and easy to use.
Difficulty in testing often exposes implicit dependencies and mixed concerns.

importsubtractorfrom'subtract';it('subtract calls _add and flips the sign of the second argument',()=>{letaddCalls=[];subtractor.add=(...args)=>{addCalls.push(args);return'subtract-response';}constresult=subtractor.subtract(5,3);expect(addCalls).toEqual([5,-3]);expect(result).toBe('subtract-response');});it('has an _add method that adds numbers',()=>{expect(subtractor._add(5,-3)).toBe(2);});

Notice that:

We need more assertions to make sure the code is working.

It’s hard to tell what the intention of subtract is because we’re mired in
its implementation details. It’s like we just wrote the inverse of the code
under test.

Also it’s very brittle. What if we refactor our code?

exportdefaultclassSubtractor{subtract(a,b){returna-b;}}

Seems like a nice improvement and it doesn’t change the public interface, but
the tests are broken.

At this point you might be thinking, “These are ridiculous, trivial cases!
There is a reason we use shallow rendering. We have complicated stuff to test
and we only want to test one component at a time.”

This brings me to the second problem.

Problem 2: Mocking private functions stifles design feedback.

I’m trying to keep these examples clear so bear with me and pretend we’ve got
some very complicated functions that may have side effects.

We want to test each function directly because they each represent a different
concern. We’ll say that testing all this behavior by only calling callIt won’t
cut it.

If we don’t change the code then we have no option but to start adding mocks.

it('callIt calls the passed function and then returns the result of mapIt',()=>{// We need to mock `mapIt` here.});it('mapIt maps over each items and decorates it with decorateIt',()=>{// We need to mock `decorateIt` here.});it('decorateIt adds asterixes around a string',()=>{});

Ugh. I can’t even bring myself to write the details of these hypothetical tests
because they’re going to be so painful.

Actually there is one I don’t mind writing. decorateIt is just a pure
function.

it('decorateIt adds asterixes around a string',()=>{expect(decorateIt('hi')).toBe('*hi*');});

mapIt is almost a pure function, but it is pulling in decorateIt from its
outer scope. callIt is not a pure function, it has a side effect because it
calls the function that was passed in and it implicitly depends on mapIt. If
only we could untangle these dependencies, this would be a lot easier to test.

We’ll untangle by moving the co-ordination of these functions into a new
function that composes them.

it('callIt calls the function and returns the result',()=>{constsomeFn=()=>'result';expect(callIt(someFn)).toBe('result');});it('mapIt maps over each item with a function',()=>{constfn=(i)=>`${i}-mapped`;expect(mapIt(['a','b'],fn).toEqual(['a-mapped','b-mapped']);});

We probably want a light test for compose that makes sure everything is wired up
correctly, but we don’t need to test edge cases there.

it('compose does all the things',()=>{constsomeFn=()=>['a','b'];expect(compose(someFn)).toEqual(['*a*','*b*']);});

This refactoring pattern shows up all the time and it goes by many names
including “dependency injection”, “matchmaker”, “strategy pattern”,
“composition” and “container vs presentation components”.

The point is, we made our code better by listening to the test pain. Does this
have any bearing on React components?

Shallow Render

This pattern is so common when building components that I’m pretty sure
most developers would wouldn’t raise an eyebrow.

Using shallow render here would make a lot of unnecessary testing work for
ourselves. We can create a test that calls <ToDoApp /> and asserts that the
result looks correct. No test pain here—seems good.

Now let’s say that the ToDoApp actually makes an AJAX call and shows a spinner
while it loads and an error if it fails. The ToDoList component filters
ToDos based on a search field. The ToDo component responds to different
events from user input (typing, clicks, etc). Now we want to test each of
these responsibilities on their own.

Shallow rendering provides a terse API for asserting on the internal
interactions between components and the components that they own. The APIs are
so nice that we would probably just write the tests and not change our code at
all.

And we would write similar tests for ToDoList and ToDo. The wrapper.equals
assertion (made possible by React’s shallow rendering) is a concise way to make an
assertion on the seam between the render function output and React’s rendering
system. Unfortunately this test is still brittle and tied to implementation
details.

Let’s do the same thing we did with our simple functions earlier and create a
component to compose the others.

Again, we’ve decomposed these components to a point that is ridiculous for the
low level of complexity we have. We don’t actually need this flexibility for
such a simple case. But even while writing these trivial tests, design questions
arise:

The ToDoLoader has no idea that it’s working with ToDos. Probably this can be
renamed and become a general purpose component.

The ToDoList has no idea what its children HTML will look like but it is
represented by a ul element that has strict rules in HTML. Either we need to
inline the ToDo element to couple this markup more tightly or make the
markup more abstract and injectable.

Because we did not immediately turn to mocks and stubs in these tests, we
created a system where the components are decoupled. This means that the design
is more flexible. For instance:

We could reuse the ToDoLoader for other components that need this loading
behavior.

We could pass in a different ToDoDecorator to render different kinds of ToDos.

The ToDoList component could easily be reused by just renaming todos and
todo to something more general.

Automocking in General

From Facebook’s documentation about shallow rendering:

Shallow rendering lets you render a component “one level deep” and assert
facts about what its render method returns, without worrying about the
behavior of child components…

To clarify the termonology, “child components” here are components that are
rendered inside of the top level element returned from the render function.
This includes both components that are passed in as props.children to the
component under test and components that are owned by the component under
test. Basically we’re mocking every component invocation except the top one.

As a demonstration, shallow rendering will happily render this component and
write a neat little test for it:

Ember followed a similar course when it took its first stab at component unit
tests in a “sandboxed” environment. Ultimately it found a better approach
by using integrated component
tests by default.

Facebook’s Jest testing library had “automocking” as a headline feature but
recently disabled it by default noting:

We introduced automocking at Facebook and it worked great for us when unit
testing was adopted in a large existing code base with few existing tests…

Many mock techniques that hide rigid design and couple themselves to
implementation details are invaluable when you need to just “get coverage” over
existing legacy code.

Buy if your use case is to just “get coverage” then why even have engineers
write the assertions? If you accept that the current code is correct, you can
easily retrofit assertions by rendering a component and then taking a snapshot
of the output. Jest calls this Snapshot Testing.

From that post;

…engineers frequently told us that they spend more time writing a test than the
component itself. As a result many people stopped writing tests altogether
which eventually led to instabilities. Engineers told us all they wanted was
to make sure their components don’t change unexpectedly.

However many engineers do not see testing as merely a tool to guard against
unexpected changes. Some engineers write unit tests because they value the
design feedback and the documented intent.

As development teams scale and as “code coverage” remains an important metric
it’s clear to see that these tools are valuable. But for new code, hear my plea:
let’s be mindful with mocking.