Testing React components has become trivial as more and more of them are now stateless functions.
The new shallow renderer and tools like Enzyme make it possible to render a component “one level deep” when testing, without even rendering it in some sort of DOM, while still having a great api to traverse the components’ output and assert facts on its behaviour.
Given all these great tools that we have, what happens when we have a container component that renders sub-components (child components) based on some sort of rule/logic?
How do we handle those dependencies to keep our unit tests free from dependencies?
A HomePage component example
import React from 'react';
import Header from './Header';
import Sidebar from './Sidebar';
import Footer from './Footer';
import { Oops } from './Oops'; // Importing a non-default export
export default function HomePage({ data, error }) {
let component = (
<div>
<Header links={data.headerLinks} />
<Sidebar />
<Footer links={data.footerLinks} />
</div>
);
if (error) {
component = (
<Oops message={error.message} />
);
}
return component;
}
The example renders Header
, Footer
and Sidebar
if there is no error, Oops
otherwise.
Testing the HomePage component
When testing the HomePage
component, we’ll probably shallow render it and check that it renders the right components based on its props.
Why shallow render is a good thing?
From the Enzyme docs:
Shallow rendering is useful to constrain yourself to testing a component as a unit, and to ensure that your tests aren’t indirectly asserting on behavior of child components.
The code:
import React from "react";
import { shallow } from "enzyme";
import HomePage from "../components/HomePage";
import Header from "../components/Header";
import Sidebar from "../components/Sidebar";
import Footer from "../components/Footer";
import { Oops } from "../components/Oops";
describe("The HomePage component", function () {
describe("when there is no error", function () {
it("should render the page", function () {
const props = {
data: {
links: "",
},
};
const output = shallow(<HomePage {...props} />);
expect(output.find(Header)).to.have.lengthOf(1);
expect(output.find(Sidebar)).to.have.lengthOf(1);
expect(output.find(Footer)).to.have.lengthOf(1);
expect(output.find(Oops)).to.have.lengthOf(0);
});
});
describe("when there is an error", function () {
it("should render the Oops", function () {
const props = {
error: {
message: "blown up",
},
};
const output = shallow(<HomePage {...props} />);
expect(output.find(Header)).to.have.lengthOf(0);
expect(output.find(Sidebar)).to.have.lengthOf(0);
expect(output.find(Footer)).to.have.lengthOf(0);
expect(output.find(Oops)).to.have.lengthOf(1);
});
});
});
This test is importing the real Header
, Footer
, Sidebar
and Oops
components, creating a dependency when it should just assume that they are behaving the way they are supposed to, so that if tomorrow the Header
component blows up, the HomePage
tests won’t be affected (the Header
tests will, though!).
Introducing proxyquire
Proxies nodejs’s require in order to make overriding dependencies during testing easy while staying totally unobstrusive.
This is how it comes to our rescue:
import React from 'react';
import { shallow } from 'enzyme';
import proxyquire from 'proxyquire';
proxyquire.noCallThru();
const Header = () => <div></div>; // stub component
const Sidebar = () => <div></div>; // stub component
const Footer = () => <div></div>; // stub component
const Oops = () => <div></div>; // stub component
// Require the component to test (HomePage) through proxyquire
// and pass it the stubbed dependencies
const HomePage = proxyquire('../components/HomePage', {
'./Header': Header,
'./Sidebar': Sidebar,
'./Footer': Footer,
'./Oops': { Oops }
}).default;
describe('The HomePage component', function () {
describe('when there is no error', function () {
it('should render the page', function () {
const props = {
data: {
links: ''
}
};
const output = shallow(<HomePage {...props} />);
expect(output.find(Header)).to.have.lengthOf(1);
expect(output.find(Sidebar)).to.have.lengthOf(1);
expect(output.find(Footer)).to.have.lengthOf(1);
expect(output.find(Oops)).to.have.lengthOf(0);
});
});
describe('when there is an error', function () {
it('should render the Oops', function () {
const props = {
error: {
message: 'blown up'
}
};
const output = shallow(<HomePage {...props} />);
expect(output.find(Header)).to.have.lengthOf(0);
expect(output.find(Sidebar)).to.have.lengthOf(0);
expect(output.find(Footer)).to.have.lengthOf(0);
expect(output.find(Oops)).to.have.lengthOf(1);
});
});
});
Code analysis:
- On line 5:
By default proxyquire calls the function defined on the original dependency whenever it is not found on the stub.
We don’t want to call any function not defined in our component, so we define a more strict behaviour.
- Lines 7-10: We define our mock components as simple stateless functions. We are not interested in what the component behaviour is, we just want to test that it receives the right props (that is being rendered correctly by the component under test).
- Lines 12-17: It is here that we pass our mocks to the
HomePage
component. We are not importing it at the top of the file, we are instead requiring it through proxyquire, passing it a set of dependencies that we want to mock.
We don’t have to mock all the HomePage
dependencies, only the ones that we want to control and we get back the HomePage
component with all those dependencies replaced by our stubs.
A note on ES6 modules
At the top of HomePage.jsx
we import the component’s dependencies:
...
import Footer from './Footer';
import { Oops } from './Oops';
...
The first line is importing a default export (export default function () { ··· }
), the second is importing a named export (export function Footer () {...}
);
// math.js
export function sum(a, b) {
return a + b;
}
export default function sub(a, b) {
return a - b;
}
// main.js
import sub from './math';
import { sum } from './math'; // or const sum = require('./math').sum;
Given the code above, the result of the exports is an object that looks like this:
import * as math from "./math";
console.log(math);
// {
// default: function sub (a, b) {...},
// sum: function sum (a, b) {...}
// }
This is why on line 19 in HomePage.proxy.spec.jsx
we take the default
object from the result returned by proxyquire: that is where the exported function is.
The syntax import Footer from './Footer'
hides the fact that under the hood the imported object is whatever is found inside the default
property of the object returned by ./Footer
.
Wrapping up
Proxyquire can mock all sorts of dependencies, this is just an example that applies to React; have a look at the examples to see how powerful it can be.
Useful links: