(in)Finite War
We have a problem. The problem with testing. The problem with testing React components, and it 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.
Everyone knows how to test simple components(they are simple), probably know how to test Applications(E2E). How to test Finite and Infinite things…
Define the Problem
There is 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 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 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 ask (looking into your current code base): How? :tableflip:?!
Today Presentation Components and Containers are not just entangled, but sometimes just not extracted as "pure" entities (hello GraphQL).
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 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:
{% twitter 1021850499618955272 %}
DI(both Dependecy 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 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.
Byconnect
ing 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 Contaiers 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
- Presentational components should only contain other presentational components.
- Containers are the Tree. Components are Tree Leafs.
- You don't have to always NOT contain Containers inside Presentational ones, but not contain them only in tests.
You may dive deeper into the problem by reading the medium article, but here let's skip all the sugar.
PS: This is a translation of ru-habr article habr version.