Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

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 betweenunit testing andintegration 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 testingcomponents, standalone libraries, and finalapplications.

You may dive deeper into the problem by reading theTesting The Finite React Components, orWhy 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, includingmount,react-testing-library,webdriver and so on. Onlyshallow is special - the rest behave in the same manner.

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

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.

Andthe 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 isnot a problem for ...dumb components. They are dumb, contains only the presentation layer, but not "side effects".

  • But that'sa problem forContainers. 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 toDan 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 onesinfinite, 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 - containslots. 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".constPageChrome=({children,aside})=>(<section><aside>{aside}</aside>{children}</section>);// test me with shallow, or real integration testconstPageChromeContainer=()=>(<PageChromeaside={<ASideContainer/>}><Page/></PageChrome>);

Approved by Dan himself:

unknown tweet media content
Dan Abramov profile image
Dan Abramov
twitter logo
@dceddia I think this is one of the biggest misunderstandings about React. It would be great to highlight this pattern more. Note how by making <Nav> and <Body> accept any elements as children, I removed the need to pass the "user" prop down through them.
20:11 PM - 24 Jul 2018
Twitter reply actionTwitter retweet action 217Twitter like action 790

DI(bothDependency Injection andDependency 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 extendSolution 1 - just declare allextension points. Just wrap them with..Boundary

constBoundary=({children})=>(process.env.NODE_ENV==='test'?null:children// or `jest.mock`);constPageChrome=()=>(<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 itfinite.

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 mocklayer, ortier, or whatever you say:

constcheckTier=tier=>tier===currentTier;constwithTier=tier=>WrapperComponent=>(props)=>((process.env.NODE_ENV!==test||checkTier(tier))&&<WrapperComponent{...props}/>);constPageChrome=()=>(<section><aside><ASideContainer/></aside><Page/></section>);constASideContainer=withTier('UI')(...)constPage=withTier('Page')(...)constPageChromeContainer=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.

Byconnecting component to Redux or GQL you are producingwell known Containers. I mean - withwell-known names -Container(WrapperComponent). You may mock them by their names

constPageChrome=()=>(<section><aside><ASideContainer/></aside><Page/></section>);// remove all components matching react-redux patternreactRemock.mock(/Connect\(\w\)/)// all any other containerreactRemock.mock(/Container/)

This approach is a bit rude - it will wipeeverything, 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"constContainerCondition=React.createContext(true);reactRemock.mock(/Connect\(\w\)/,(type,props,children)=>(<ContainerCondition.Consumer>{opened=>(opened?(// "close" and render real component<ContainerCondition.Providervalue={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 keeptight coupling usingdefaultProps, and nullify these props in tests...

constPageChrome=({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 onegear out of thegearbox. 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 inhis 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 toalways NOT contain Containers inside Presentational ones, butnot contain them only in tests.

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

Top comments(4)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
dance2die profile image
Sung M. Kim
Exploring the world of TypeScript, React & Node
  • Location
    NYC - the Big 🍎
  • Education
    SUNY Stony Brook
  • Joined

Thanks, Anton for the thorough post.

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

CollapseExpand
 
thekashey profile image
Anton Korzunov
Reinventing the wheels.
  • Location
    Sydney
  • Work
    Foreign Contaminant at Atlassian
  • Joined

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

CollapseExpand
 
dance2die profile image
Sung M. Kim
Exploring the world of TypeScript, React & Node
  • Location
    NYC - the Big 🍎
  • Education
    SUNY Stony Brook
  • Joined

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

CollapseExpand
 
pablogot profile image
Pablo
  • Joined
• Edited on• Edited

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

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Reinventing the wheels.
  • Location
    Sydney
  • Work
    Foreign Contaminant at Atlassian
  • Joined

More fromAnton Korzunov

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp