The End Components Of React.js
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.
The Issue
The problem is very simple – unit tests. Recently there is some movement in the direction of the 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. The ability to start tests with some ready 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 without any problems.
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 see no problems here. On the first point, of course, we can recommend integration tests, they were invented for this purpose – to check how pre-tested components were 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 little more complicated. And this article will be about this item (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, is well described and has already managed to become outdated. If you take Dan Abramov’s article as a basis (as 99% of developers do), 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 container components ** inside, and usually have some DOM markup and styles of their own),
- 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 data (Don’t specify 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 SFCs (life written hooks, or performance optimizations).
The 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 the definition of Dan are two – “Do not depend on the application”, which is almost the academic definition of a “unit”, and * “May contain both other presentation components and containers **” * where these asterisks are particularly interesting.
In earlier versions of my article, I said that the presentation of the components should contain only other presentation components. I don’t think so anymore. The component type is detail 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 the 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 application state 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 that will be used in your non-Containers are changed 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 since 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(<span class="hljs-string">'react-cool-tooltip'</span>, {<span class="hljs-attr">default</span>: <span class="hljs-function">(<span class="hljs-params">{children}</span>) =></span> childlren});
But if you click react-cool-tooltip, then there will be no problems with testing. “Component” has become sharply dumber, much shorter, much more 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, since they contain an uncontrolled state and “increase” the size, i.e. make the current component infinite.
- Otherwise, this is the usual presentation component. In fact, it was exactly as 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.
// I test via mount if slots are empty const PageChrome = ({children, aside}) => ( <section> <aside> {aside} </ aside> {children} </ section> ); // and I test through shallow, just check that the slots are transferred // maybe it will work through mount? once, so, cleanly check wiring? const PageChromeContainer = () => ( <PageChrome aside = {<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 // // or 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 can 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 the 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 them?
The “containers” that we don’t like so much are usually called containers. And if not – nothing prevents right now to start calling Components as something more sonorous. Or they have a certain pattern in the name – Connect (WrappedComonent), or GraphQL/Query.
What if right in ran time 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'; // initially "can" const ContainerCondition = React.createContext (true); reactRemock.mock (/ Connect \ (\ w \) /, (type, props, children) => ( <ContainerCondition.Consumer> {opened => ( opened ? ( // "close" and render the real component <ContainerCondition.Provider value = {false}> {createElement (type, props, ... children)} <ContainerCondition.Provider> ) // "closed" : null )} </ContainerCondition.Consumer> )
Conclusion
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 in 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.