End React Components
What I like about the React ecosystem is that behind many solutions there is an IDEA. Different authors write various articles in support of the existing order and explain why everything is “right”, so everyone understands that the party is on the right track.
After some time the IDEA changes a little, and everything starts from the beginning.
And the beginning of this story is the separation of components into Containers and non-Containers (in the people - Stupid Components, sorry for my French).
Problem
The problem is very simple - unit tests. Recently there is some movement in the direction of integrations tests - well, you know, "Write tests. Not too many. Mostly integration." . The idea is not bad, and if time is short (and tests are not particularly needed), this is how it should be done. Just let's call it smoke tests - purely to check that nothing seems to explode.
If there is a lot of time, and tests are needed - this road is better not to go, because writing good integration tests is very, very LONG. Just because they will grow and grow, and in order to test the third button on the right, it will be necessary at the beginning to click on the 3 buttons in the menu, and not to forget to log in. In general - here's a combinatorial explosion on a saucer.
The solution here is one and simple (by definition) - unit tests. Ability to start tests with some already finished state of some part of the application. Or rather, to reduce (narrow) the field of testing from an Application or a Big Block to something small — a unit, whatever it is. It does not necessarily use the enzyme - you can run browser tests, if the soul asks. The most important thing here is to be able to test something in isolation . And no problem.
Isolation is one of the key points in unit testing, and that’s why units don’t like tests. They do not like for various reasons:
- for example, your "unit" is detached from the application, and does not work in its composition even when its own tests are green.
- or, for example, because isolation is such a spherical horse in a vacuum that no one has seen. How to achieve it, and how to measure it?
Personally, I do not see problems here. On the first point, of course, we can recommend integration tests, they are designed to do this - to check how the previously tested components are correctly assembled. You trust npm packages that test, of course, only themselves, and not themselves as part of your application. How do your "components" differ from "not your" packages?
With the second paragraph, everything is a bit more complicated. And this article will be about this point (and everything before this was so - an introduction) - about how to make a "unit" unit testable .
Divide and rule
The idea of separating the React component into "Container" and "Presentation" is not new, well described, and has already managed to become outdated. If we take as a basis (what 99% of developers do) the article by Dan Abramov , then the Presentation Component:
- Are responsible for appearance (Are concerned with how things look)
- May contain both other presentation components and containers
**
(May contain both presentational and containerized**
inside). - Support slots (Often allow containment via this.props.children)
- Do not depend on the application (Have no dependencies on the rest of the app, such as Flux actions or stores)
- Do not depend on the data (not the data is loaded or mutated)
- Interface based on props (Receive data and callbacks exclusively via props)
- Often stateless (Rarely have their own state (when they do, it's UI state rather than data))
- Often SFC (Are written as functional components, lifecycle hooks, or performance optimizations)
Well, Containers are all logic, all data access, and all application in principle.
In an ideal world, the containers are the trunk, and the presentation components are the leaves.
The key points in Dan’s definition of two are “Do not depend on the application” , which is almost the academic definition of a “unit”, and * “May contain other presentation components as well as containers **
” * where these asterisks are particularly interesting.
(free translation) ** In earlier versions of my article, I (Dan) said that the presentation of the components should contain only other presentation components. I don't think so anymore. Component type is a part and may change over time. In general, do not worry and everything will be okay.
Let's remember what happens after this:
- In the storybook, everything falls, because some container, in the third button on the left, climbs into the page which is not. Special greetings graphql, react-router and other react-intl.
- You lose the ability to use mount in tests, because it renders everything from A to Z, and again, somewhere in the depths of the render tree, someone does something and the tests drop.
- The ability to control the state of the application is lost, since (figuratively speaking) the opportunity to switch selectors / resolvers is lost (especially with proxyquire), and the entire page is required to be wet. And this is cool for unit tests.
If it seems to you that the problems are a bit contrived - try to work in a team, when these containers to be used in your non-containers change in other departments, and as a result, you look at the tests and you cannot understand why yesterday worked, and here again.
As a result, you have to use shallow, which by design eliminates all harmful (and unexpected) side effects. Here is a simple example from the article "Why I always use shallow"
Imagine that the Tooltip will render "?", When clicked, the type itself will be shown.
import Tooltip from'react-cool-tooltip';
const MyComponent = () => {
<Tooltip>
hint: {veryImportantTextYouHaveToTest}
</Tooltip>
}
How to protest it? Mount + click + check what is visible. This is an integration test, not a unit, and the question is how to click on the "alien" component for you. There is no problem with shallow, as there are no brains and the "alien component" itself. And there are brains here, since Tooltip is a container, while MyComponent is practically presentation.
jest.mock('react-cool-tooltip', {default: ({children}) => childlren});
But if you click react-cool-tooltip, then there will be no problems with testing. "Component" has become sharply dumber, much shorter, much finite .
Final component
- a component with a well-known size, which may include other, previously known, final components, or not containing them at all.
- does not contain other containers, as they contain uncontrolled state and "increase" the size, i.e. make the current component infinite .
- otherwise, this is the usual presentation component. In fact, exactly as it was described in the first version of Dan’s article.
The final component is just a gear, taken out of a large mechanism.
The whole question is how to take it out.
Solution 1 - DI
My favorite is Dependency Injection. Dan loves him too . In general, it is not DI, but "slots". In a nutshell - no need to use Containers inside Presentation - they need to be injected there . And in the tests it will be possible to inject something else.
// я тестируем через mount если слоты сделать пустымиconst PageChrome = ({children, aside}) => (
<section><aside>{aside}</aside>
{children}
</section>
);
// а я тестируем через shallow, просто проверь что в слоты переданы// а может и через mount сработает? разок, так, чисто проверить wiring?const PageChromeContainer = () => (
<PageChromeaside={<ASideContainer />}>
<Page /></PageChrome>
);
This is the case when "the containers are the trunk and the presentation components are the leaves"
Solution 2 - Borders
DI can often be cool. Probably now%% username% thinks how it can be applied on the current code base, and the solution is not invented ...
In such cases, you will save the boundaries .
const Boundary = ({children}) => (
process.env.NODE_ENV === 'test' ? null : children
// // или jest.mock
);
const PageChrome = () => (
<section><aside><Boundary><ASideContainer /></Boundary></aside><Boundary><Page /></Boundary></section>
);
Here, instead of "slots", just all the "transition points" turn into Boundary, which will render anything during the tests. Pretty declarative , and exactly what you need to "take out the gear."
Solution 3 - Tier
Borders can be a little rough, and it may be easier to make them a little smarter by adding a little knowledge about Layer.
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);
Under the name Tier / Layer there may be different things - feature, duck, module, or just what layer / tier. The essence is not important, the main thing is that you can pull out a gear, perhaps not one, but a finite amount, somehow drawing the line between what is needed and what is not needed (for different tests this is a different line).
And nothing prevents to mark these boundaries somehow differently.
Solution 4 - Separate Concerns
If the solution (by definition) lies in the separation of essences - what will happen if we take them and divide?
The “containers” that we don’t like so much are usually called containers . And if not, nothing prevents right now to start calling Components somehow more sonorous. Or they have a certain pattern in the name - Connect (WrappedComonent), or GraphQL / Query.
What if right in rantayma draw a boundary between entities based on the name?
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/)
Plus one line in the tests, and react-remock will remove all containers that might interfere with the tests.
In principle, this approach can be used to test the containers themselves - just need to remove everything except the first container.
import {createElement, remock} from'react-remock';
// изначально "можно"const ContainerCondition = React.createContext(true);
reactRemock.mock(/Connect\(\w\)/, (type, props, children) => (
<ContainerCondition.Consumer>
{ opened => (
opened
? (
// "закрываем" и рендерим реальный компонент
<ContainerCondition.Provider value={false}>
{createElement(type, props, ...children)}
<ContainerCondition.Provider>
)
// "закрыто"
: null
)}
</ContainerCondition.Consumer>
)
Again - a couple of lines and gear removed.
Total
Over the past year, testing of the React component has become more complicated, especially for the mount — all 10 Providers, Contexts need to be turned, and it is becoming more and more difficult to test the necessary component in the required state — too many strings to pull.
Someone spits and goes into the shallow world. Someone waves his hand at unit tests and transfers everything to Cypress (to walk so to walk!).
Someone else pokes a finger into the reactor, says that it is algebraic effects and you can do what you want. All the examples above are essentially the use of these algebraic effects and mocks. For me and DI this is moki.
PS: This post was written as an answer to a comment in React / RFC about the fact that the React team broke everything, and all the polymers there too.
PPS: This post is actually a very free translation of another
PPPS: In general, for real isolation, look at rewiremock