DEV Community

Cover image for (in)Finite War
Anton Korzunov
Anton Korzunov

Posted on • Edited on

(in)Finite War

We have a problem

The problem with testing React components is quite fundamental. It’s about the difference between unit testing and integration testing. It’s about the difference between what we call unit testing and what we call integration testing, the size and the scope.

It's not about testing itself, but about Component Architecture. About the difference between testing components, standalone libraries, and final applications.

You may dive deeper into the problem by reading the Testing The Finite React Components, or Why I Always Use Shallow Rendering, but here let's skip all the sugar.

Define the Problem

There are 2 different ways to test React Component - shallow and everything else, including mount, react-testing-library, webdriver and so on. Only shallow is special - the rest behave in the same manner.

And this difference is about the size, and the scope - about WHAT would be tested, and just partially how.

In short - shallow will only record calls to React.createElement, but not running any side effects, including rendering DOM elements - it's a side(algebraic) effect of React.createElement.

Any other command will run the code you provided with each and every side effect also being executed. As it would be in real, and that's the goal.

And the problem is the following: you can NOT run each and every side effect.

Why not?

Function purity? Purity and Immutability - the holy cows of today. And you are slaughtering one of them. The axioms of unit testing - no side effects, isolation, mocking, everything under control.

  • But that's is not a problem for ... dumb components. They are dumb, contains only the presentation layer, but not "side effects".

  • But that's a problem for Containers. As long they are not dumb, contains whatever they want, and fully about side effects. They are the problem!

Probably, if we define the rules of "The Right Component" we could easily test - it will guide us, and help us.

TRDL: The Finite Component

Smart and Dumb components

According to Dan Abramov Article Presentation Components are:

  • Are concerned with how things look.
  • May contain both presentational and container components** inside, and usually have some DOM markup and styles of their own.
  • Often allow containment via this.props.children.
  • Have no dependencies on the rest of the app, such as Flux actions or stores.
  • Don’t specify how the data is loaded or mutated.
  • Receive data and callbacks exclusively via props.
  • Rarely have their own state (when they do, it’s UI state rather than data).
  • Are written as functional components unless they need state, lifecycle hooks, or performance optimizations.
  • Examples: Page, Sidebar, Story, UserInfo, List.
  • ....
  • And Containers are just data/props providers for these components.

According to the origins: In the ideal Application…
Containers are the Tree. Components are Tree Leafs.

Find the black cat in the dark room

The secret sauce here, one change we have to amend in this definition, is hidden inside “May contain both presentational and container components**, let me cite the original article:

In an earlier version of this article I claimed that presentational components should only contain other presentational components. I no longer think this is the case. Whether a component is a presentational component or a container is its implementation detail. You should be able to replace a presentational component with a container without modifying any of the call sites. Therefore, both presentational and container components can contain other presentational or container components just fine.

Ok, but what about the rule, which makes presentation components unit testable – “Have no dependencies on the rest of the app”?

Unfortunately, by including containers into the presentation components you are making second ones infinite, and injecting dependency to the rest of the app.

Probably that's not something you were intended to do. So, I don't have any other choice, but to make dumb component finite:

PRESENTATION COMPONENTS SHOULD ONLY CONTAIN OTHER PRESENTATION COMPONENTS

And the only question, you should as: How?

Solution 1 - DI

Solution 1 is simple - don't contain nested containers in the dumb component - contain slots. Just accept "content"(children), as props, and that would solve the problem:

  • you are able to test the dumb component without "the rest of your app"
  • you are able to test integration with smoke/integration/e2e test, not tests.
// Test me with mount, with "slots emty".
const PageChrome = ({children, aside}) => (
  <section>
    <aside>{aside}</aside>
    {children}
  </section>
);

// test me with shallow, or real integration test
const PageChromeContainer = () => (
  <PageChrome aside={<ASideContainer />}>
    <Page />
  </PageChrome> 
);

Approved by Dan himself:

DI(both Dependency Injection and Dependency Inversion), probably, is a most reusable technique here, able to make your life much, much easier.

Point here - Dumb components are dumb!

Solution 2 - Boundaries

This is a quite declarative solution, and could extend Solution 1 - just declare all extension points. Just wrap them with.. Boundary

const Boundary = ({children}) => (
  process.env.NODE_ENV === 'test' ? null : children
  // or `jest.mock`
);

const PageChrome = () => (
  <section>
    <aside><Boundary><ASideContainer /></Boundary></aside>
    <Boundary><Page /></Boundary>
  </section>
);

Then - you are able to disable, just zero, Boundary to reduce Component scope, and make it finite.

Point here - Boundary is on Dumb component level. Dumb component is controlling how Dumb it is.

Solution 3 - Tier

Is the same as Solution 2, but with more smart Boundary, able to mock layer, or tier, or whatever you say:

const checkTier = tier => tier === currentTier;
const withTier = tier => WrapperComponent => (props) => (
  (process.env.NODE_ENV !== test || checkTier(tier))
   && <WrapperComponent{...props} />
);
const PageChrome = () => (
  <section>
    <aside><ASideContainer /></aside>
    <Page />
  </section>
);
const ASideContainer = withTier('UI')(...)
const Page = withTier('Page')(...)
const PageChromeContainer = withTier('UI')(PageChrome);

Even if this is almost similar to Boundary example - Dumb component is Dumb, and Containers controlling the visibility of other Containers.

Solution 4 - Separate Concerns

Another solution is just to Separate Concerns! I mean - you already did it, and probably it's time to utilize it.

By connecting component to Redux or GQL you are producing well known Containers. I mean - with well-known names - Container(WrapperComponent). You may mock them by their names

const PageChrome = () => (
  <section>
    <aside><ASideContainer /></aside>
    <Page />
  </section>
);

// remove all components matching react-redux pattern
reactRemock.mock(/Connect\(\w\)/)
// all any other container
reactRemock.mock(/Container/)

This approach is a bit rude - it will wipe everything, making harder to test Containers themselves, and you may use a bit more complex mocking to keep the "first one":

import {createElement, remock} from 'react-remock';

// initially "open"
const ContainerCondition = React.createContext(true);

reactRemock.mock(/Connect\(\w\)/, (type, props, children) => (
  <ContainerCondition.Consumer>
   { opened => (
      opened
       ? (
         // "close" and render real component
         <ContainerCondition.Provider value={false}>
          {createElement(type, props, ...children)}
         <ContainerCondition.Provider>
         )      
       // it's "closed"
       : null
   )}
  </ContainerCondition.Consumer>
)

Point here: there is no logic inside nor Presentation, not Container - all logic is outside.

Bonus Solution - Separate Concerns

You may keep tight coupling using defaultProps, and nullify these props in tests...

const PageChrome = ({Content = Page, Aside = ASideContainer}) => (
  <section>
    <aside><Aside/></aside>
    <Content/>
  </section>
);

So?

So I've just posted a few ways to reduce the scope of any component, and make them much more testable. The simple way to get one gear out of the gearbox. A simple pattern to make your life easier.

E2E tests are great, but it's hard to simulate some conditions, which could occur inside a deeply nested feature and be ready for them. You have to have unit tests to be able to simulate different scenarios. You have to have integration tests to ensure that everything is wired properly.

You know, as Dan wrote in his another article:

For example, if a button can be in one of 5 different states (normal, active, hover, danger, disabled), the code updating the button must be correct for 5×4=20 possible transitions — or forbid some of them. How do we tame the combinatorial explosion of possible states and make visual output predictable?

While the right solution here is state machines, being able to cherry-pick a single atom or molecule and play with it - is the base requirement.

The main points of this article

  1. Presentational components should only contain other presentational components.
  2. Containers are the Tree. Components are Tree Leafs.
  3. You don't have to always NOT contain Containers inside Presentational ones, but not contain them only in tests.

PS: I would recommend reading (auto-translated) habr version of this post.

Top comments (4)

Collapse
 
dance2die profile image
Sung M. Kim

Thanks, Anton for the thorough post.

Would you be able to update the code formatting in Solution 4 - Separate Concerns?

Collapse
 
thekashey profile image
Anton Korzunov

Sorry mate. One unclosed "`", left after refactoring, could ruin everything.

Collapse
 
dance2die profile image
Sung M. Kim

No worries, mate. I messed up many times with formatting as well 😂
And thanks for the update~

Collapse
 
pablogot profile image
Pablo • Edited

Thank you Anton for the post, great explanation and examples here :)