📜 ⬆️ ⬇️

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 towards 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 verify 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, you will have to click on the 3 buttons in the menu at the beginning, and remember 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:



Personally, I see no problems here. On the first point, of course, we can recommend integration tests, they are invented for this purpose - 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 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 we take as a basis (what 99% of developers do) the article by Dan Abramov , then the Presentation Component:



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 both other presentation components and containers ** ” * where these stars 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 detail and may change over time. In general, do not worry and everything will be okay.

Let's remember what happens after this:



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, 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('react-cool-tooltip', {default: ({children}) => childlren}); 

But if you click react-cool-tooltip, then there will be no testing problems. "Component" has become sharply dumber, much shorter, much more finite .


Final component



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 = () => ( <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 // //  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?


“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 (walk for a 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

')

Source: https://habr.com/ru/post/434830/


All Articles