Testing The Finite React Components

Written by antonkorzunov | Published 2018/12/21
Tech Story Tags: javascript | react | testing | finite-react-components | react-testing

TLDRvia the TL;DR App

Or limited depth components, or components with well know boundaries, or isolated components, or scoped components or components you just can just test. And be happy with testing.

Define the problem

The problem with testing React components 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. And, while for ages they were no more than different parts of The Testing, now there is a WAR of different testing philosophies.

  • E2E Integration testing. Browser based testing from one End of your’s application — the entry point,— and to another End — the deepest deeps. Testing your app as your customer would do. Testing a full spectrum – UI, styles, backend. The only testing kind, which would actually test your application, that’s why it’s so cool.
  • Unit tests. Testing a very small(or scoped) components, usually in isolated or fully mocked environment. Testing nothing but a single and simple “thing” – a unit, “thing” by “thing”, that’s why its easy to write and ship a well tested “thing” – it’s unbound from the rest of the application

Which? When?

And here are the philosophical problems:

  • You cannot use integration tests before your application is ready – these tests are to ensure that app is working correctly, but it does not YET. You probably could keep them red, to guide you in a TDD manner, but you should not rely on implementation details you haven’t written yet. And yes — label on the button could be considered as implementation detail, unless design is well established, and all button labels are known. Usually they are, but if you are working in a big team(or productive manager) – details are the subject to change.
  • Unit tests could be created before application, which would lead to premature testing coverage, and you will probably redo everything in the future, which is what YAGNY about — You aren’t gonna need it.

In other words — unit and integration tests have their own benefits and issues, and are not interchangeable, and aimed not to solve different tasks, but to work on different scopes — low component level and high application level. Just because E2E Test approach is an overpower for a component level, while unit testing frameworks are not capable to handle a thing bigger than a module (but you will try).

Testing pyramid

Symbiosis between units tests and integration — this is what Testing Pyramid should mean. E2E test to ensure cross stack connectivity, Integrations tests, for testing how your application was assembled, and Unit tests, for testing how each and every component work.

But you can not test that “each and every” anywhere, except the unit level due to “combinatorial explosion”.

Let’s imagine you have one component with 2 different states. Only 2 cases to test. Let’s imagine you have 2 components to test — 2x2=4! Let’s imagine 3 components. 3x2 = 6! Just a 6 test cases.

You can test every component by it’s own, independently, and just sum all the test cases to got the required number of tests — linear O(n) growth. For integration tests 3 components already formed 8(2x2x2) combinations, as long as you are testing them simultaneously, and this EXPLOSION — O(n²).

So the rule is simple — use unit tests to test components in isolation, one-by-one, and integration/E2E to test them together. Not to test — ensure that combination is “right”, and wired properly— you may tests happy path only, for example. Or even smoke tests.

Having only integration testing is less than ideal though as unit tests help us design more robust software by easily testing alternate code and failure paths. We should save integration tests for larger “does it really work” kinds of tests. SendGrid

Unit tests are great, as long as in the end you can assemble your application from well unit tested pieces, and then verify the result by a integration test.

This is something I like about node modules – well unit tested pieces, I could rely on.

And the point of everything above is super simple:

  • You can’t skip units testing. They have their purpose.
  • You have to use unit tests, to make your app predictable in unpredicted conditions.
  • You shall unit test your application. For the sake of quality.

But there is a problem...

The Unit Problems

First problem — Enzyme vs Webdriver

The majority thinks that Enzyme, to be more concrete shallow, is the unit way to unit test. That’s partially true — shallow gives you ability to test only your component, as long as it does not perform a “full” render.

shallow is a way to unit test only your piece of code. But there are so many disadvantages of shallow testing, like RenderProps or Context incompatibility.

Mount does not have these disadvantages, but has others, like testing everything below mount point. Usually it’s a blocker, as long as it common today that something below you, will require something above you — Redux Store, I18nProvider, Context, Apollo, Styled Theme… just everything. And not just to exists, but to have right values.

For a last year I was adding one more wrapper for my test every month.

I would just ask — if you have a simple Button component, is there any difference between shallow and mount?

const Button = ({children, onClick}) => (<button onClick={onClick} className='my-button'>{children}</button>)

😉 — for small components there is no difference between shallow, which just record your React.createElement (result of JSX transpilation) calls, and mount, executed these calls, ie performs a full render. And even there is no difference from browser based tests like webdriver or Cypress. For small components they all the same. For small component the same code could work everywhere. Why?

Why??!!

Second problem — define unit

So — the game changer here is WHAT we call a UNIT we could easily unit test, and “why”.

I mean — you can unit test some component, cos it’s unit testable. Very obviously. Why they are unit testable? Cos the are small? Probably no, this time it’s not so obvious.

Probably, if we define what does “unit” means — we would be able to understand how to archive a better unit tests by making our components more unit, more finite. You probably already understand the idea from the title of this article, but..

Finite React Components

React Presentation Components, or Dumb Components — they are quite heavy described in the past, and separation between Smart and Dumb Components was(and is) a Big Thing, especially in Redux applications.

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 a data/props providers for these components.

In the ideal Application…

Containers are the Tree. Components are Tree Leafs.

The secret sauce here, a 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 inside presentation components you are making second infinite, and injecting dependecy to the rest of the app.

You may know how to mock, setup and feed your container, and how to render your presentation component, but if it will contain another container, or(usually) containers — you will be unable to unit test your one. It will be already integration tests, and it would require much more work to setup proper environment for ALL containers you actually used and assert the result.

So — it’s simple — PRESENTATION COMPONENTS SHOULD ONLY CONTAIN OTHER PRESENTATION COMPONENTS.

And then — you will be able to unit test your container or your dumb component as a separated, isolated, scoped, and finite thing — A UNIT.

By removing containers from presentation layer you might make your tests easier. But, probably right now you are looking on your code, with lots of containers nested inside Dumb components, and the question you have — “HOW”

How??!!

Finity War

Solution 1 — Dependency Injection

This is my favourite one. DI, Slots and Rock-n-Roll.

If you need to contain another Container inside Presentation Component — pass it as children or another slot prop. As result you will be able to test it with these slots empty. In this case it would be finite.

// test me with mount and empty slotsconst PageChrome = ({children, aside}) => (<section><aside>{aside}</aside>{children}</section>);

// test me with shallow, I am shallow testableconst PageChromeContainer = () => (<PageChrome aside={<ASideContainer />}><Page /></Page>);

DI probably is the most reusable solution you can imagine — you are free to “wire” your application differently. This is programing technique, which may make better both your code, and testing.

☝️Containers are the Tree. Components are Tree Leafs.

Solution 2 — The Boundary

DI sometimes could be a bit overpower. But what if you will able to contain Containers inside Presentation in Production, but not in Testing environment?

const Boundary = ({children}) => (process.env.NODE_ENV === 'test' ? null : children);

const PageChrome = () => (<section><aside><Boundary><ASideContainer /></Boundary></aside><Boundary><Page /></Boundary></section>);

const PageChromeContainer = () => (<PageChrome /> // lets assume that we still need this container :P);

Here it would work “as expected” in dev or prod, but in test env it will not render nested Containers, making PageChrome finite. Just add more granule control over Boundary — and that’s the deal.

☝️Presentation Component will not contain Containers. But only in test environment.

Solution 2.5 — Multitier Boundary

Almost the same, but with a taste of Layered Architecture — just for every container define Tier, and if Tier is not matching the “current” one — do not render it.

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(2)(...)const Page = withTier(3)(...)

const PageChromeContainer = withTier(1)(PageChrome);

Tier here could be almost anything — module, feature, actually “Tier”. In this case Presentation Component could look as “usual”, and Containers themselves would decide should they exists during the tests, or not.

Then you are testing a Component — it’s probably a part of a feature, and could contain another containers from the same feature. You may keep containers you are aware of, and remove others.

Let me cite Dan’s Article yet again:

Remember, components don’t have to emit DOM. They only need to provide composition boundaries between UI concerns.

Feel free to add these boundaries.

Solution 3 — Separate Concerns

The base of concern separation is actually — separation, and your ability to distinguish one thing, from another. Usually it could be done according to the components name.

Let’s assume, that all our Containers has a pattern in their name, and that’s true for the ones, connected to Redux — they all are Connect(ComponentName) .

Redux is quite good example of the finite idea — “connect” is the beginning of everything and the end. It’s a Boundary by design.

const PageChrome = () => (<section><aside><ASideContainer /></aside><Page /></section>);

const PageChromeContainer = connect()(PageChrome);

// remove all components matching react-redux patternreactRemock.mock(/Connect\(\w\)/)

Using this approach you will be able to test PageChrome, but not PageChromeContainer as long as it would be also removed. Let’s create a better example:

import {createElement, remock} from 'react-remock';

// initially allowedconst ContainerCondition = React.createContext(true);

reactRemock.mock(/Connect\(\w\)/, (type, props, children) => (<ContainerCondition.Consumer>{ opened => (opened? (<ContainerCondition.Provider value={false}>// "close" and render real element{createElement(type, props, ...children)}<ContainerCondition.Provider>): null)}</ContainerCondition.Consumer>)

It’s a bit more complex — replaces every connect with React.Context based condition, which would render only first encountered container, any nested one would be rendered as null , thus — your Presentation Components will become “finite” in the testing environment, without any actual code change.

PS: reactRemock here is https://github.com/theKashey/react-remock

In the end

By the end of a day — you will establish a well known boundaries across different entities, layers, features, modules, or components separated by any other principle. You will be able to use any testing tool — shallow, mount or even webdriver based tools — the tool will not matter anymore.

There are different ways to achive it — more declarative and explicit, like DI, or invisible like remocking. It will not only give you a better testing, but you will be able to create a more scoped Storybooks.

Test the thing you build, focus on details, pick gear by gear. And the only thing you need for it — ability to “pick” a single gear and test it. A single gear, not all the gearbox.

Just Boundaries and Separation.

PS: And yet again — you may test a single gear, and a whole gearbox — but you cant test a half of gearbox, starting from some random stuff in the middle.

  • Read more about containers/presentation

Presentational and Container Components_You’ll find your components much easier to reuse and reason about if you divide them into two categories._medium.com

  • Read more about shallow rendering

Why I Always Use Shallow Rendering_Tests should help me be confident that my application is working and there are better ways to do that than shallow…_hackernoon.com

  • Read more about power of mocking

SSR: Dependency mocking is the answer!_Or breaking free from side effects and singletons in nodejs and webpack. Long story short – let me explain some things…_hackernoon.com

PS: This article was written as a response to this comment by Dave Schinkel on React RFC. Good testing is not bound to React API Dave!

contextType: convenience API for reading context in a class by gaearon · Pull Request #65 ·…_View formatted RFC Note: I just wrote up the RFC. The semantics were designed by @sebmarkbage._github.com


Published by HackerNoon on 2018/12/21