describe-component v1.1.0
describe-component
describe-component
is an easy-to-use React unit testing library that removes
all your boilerplate code from your tests.
It codifies a pattern for unit testing using
Enzyme so that your tests are all
consistently written. With describe-component
, anyone can read, understand,
and change a unit test for a React component.
Here's what it looks like. This example is for Jest, but
describe-component
also works in Mocha, Jasmine,
AVA, or
any other test framework with beforeEach and afterEach.
import React from "react";
import describeComponent from "describe-component/jest";
const ColorableDiv = ({ color, children }) => (
<div data-component-name="ColorableDiv" style={color ? { color } : undefined}>
{children}
</div>
);
describeComponent(ColorableDiv, ({ mountWrapper: colorableDiv, setProps }) => {
it("renders a div", () => {
expect(colorableDiv().find("div")).toHaveLength(1);
});
it("sets the data-component-name attribute on that div to 'ColorableDiv'", () => {
expect(colorableDiv().find("div").props()).toMatchObject({
"data-component-name": "ColorableDiv",
});
});
describe("with children", () => {
beforeEach(() => {
setProps({ children: <span id="some-child" /> });
});
it("passes its children to the div", () => {
expect(colorableDiv().find("#some-child")).toHaveLength(1);
});
});
describe("with a color", () => {
beforeEach(() => {
setProps({ color: "red" });
});
it("sets the inline style of the div", () => {
expect(colorableDiv().find("div").props().style).toMatchObject({
color: "red",
});
});
});
describe("with no color", () => {
it("sets no inline styles", () => {
const style = colorableDiv().find("div").props().style;
expect(style).not.toBeDefined();
});
});
});
Here's how it works:
- You call
describeComponent
with a component class or function and a callback. - Your callback is synchronously called with a set of helper functions.
- In your callback, you use the
setProps
andclearProps
helpers to set the props you want to render your component with. - In your callback, you call
mountWrapper
,shallowWrapper
, orrenderWrapper
to use Enzyme'smount
,shallow
, orrender
functions on the component you are testing, using the props you set earlier.
Installation
With yarn:
$ yarn add --dev describe-component
With npm:
$ npm install --save-dev describe-component
If you don't have them already, you will also need React and Enzyme installed. See the Enzyme installation instructions for info on how to install Enzyme.
Documentation
describeComponent
describeComponent(Component, callbackFunction) => undefined
Creates a wrapping describe
for the Component's display name, sets up a bunch
of boilerplate, and calls back your callbackFunction
.
// This...
describeComponent(FishCake, ({ mountWrapper: fishCake, setProps }) => {
// Tests go here
});
// ... is roughly the same as this:
import { mount } from "enzyme";
describe("FishCake", () => {
let props;
let mountedFishCake;
beforeEach(() => {
props = {};
mountedFishCake = undefined;
});
afterEach(() => {
if (mountedFishCake) {
mountedFishCake.unmount();
}
});
const setProps = (newProps) => {
Object.assign(props, newProps);
};
const fishCake = () => {
if (!mountedFishCake) {
mountedFishCake = mount(
<FishCake {...props} />
);
}
return mountedFishCake;
};
// Tests go here
});
Your callbackFunction
gets called with an object that has the
following helper methods on it: mountWrapper
,
shallowWrapper
, renderWrapper
,
setProps
, clearProps
, and props
.
mountWrapper
mountWrapper([enzymeOptions]) => ReactWrapper
A wrapper around Enzyme's mount
that will mount your component (using the props set by setProps
and
clearProps
), and return the
ReactWrapper
created by mount
.
// This...
const card = mount(<PlayingCard kind="7" suit="CLUBS" />);
// ... is roughly the same as this:
describeComponent(PlayingCard, ({ mountWrapper, setProps }) => {
setProps({ kind: "7", suit: "CLUBS" });
const card = mountWrapper();
});
If present, the options passed into mountWrapper will be passed into Enzyme's
mount
as the second argument.
// This...
const card = mount(
<PlayingCard kind="7" suit="CLUBS" />,
{ context: { kind: "bicycleBlue" } }
);
// ... is roughly the same as this:
describeComponent(PlayingCard, ({ mountWrapper, setProps }) => {
setProps({ kind: "7", suit: "CLUBS" });
const card = mountWrapper({ context: { kind: "bicycleBlue" } });
});
mountWrapper
is memoized, so it will only call mount
once per test, and
subsequent calls will return the first ReactWrapper instance.
mountWrapper() === mountWrapper(); // true
This means that you can use mountWrapper
as many times as you want without any
performance penalty:
expect(mountWrapper().find(".upper-left-card-label").text()).toBe("7");
expect(mountWrapper().find(".bottom-right-card-label").text()).toBe("7");
Usually when you use mountWrapper
, you rename it so that its name matches the
name of the component under test, but written in lowerCamelCase:
describeComponent(NailPolish, ({ mountWrapper: nailPolish, setProps }) => {
it("uses its color prop as the background color for the .bottle-contents it renders", () => {
setProps({ color: "firebrick" });
expect(nailPolish().find(".bottle-contents").props().style.backgroundColor).toBe("firebrick");
});
});
The ReactWrapper
created by mountWrapper
will be unmounted automatically
after each test.
shallowWrapper
shallowWrapper([enzymeOptions]) => ShallowWrapper
A wrapper around Enzyme's shallow
that will shallow-render your component (using the props set by setProps
and clearProps
), and return the
ShallowWrapper
created by shallow
.
// This...
const flavor = shallow(
<CupcakeFlavor sweetness={6} saltiness={2} name="Salted Caramel" />
);
// ... is roughly the same as this:
describeComponent(CupcakeFlavor, ({ shallowWrapper, setProps }) => {
setProps({ sweetness: 6 saltiness: 2 name: "Salted Caramel" });
const flavor = shallowWrapper();
});
If present, the options passed into shallowWrapper will be passed into Enzyme's
shallow
as the second argument.
// This...
const flavor = shallow(
<CupcakeFlavor sweetness={6} saltiness={2} name="Salted Caramel" />,
{ context: { linerColor: "pink" } }
);
// ... is roughly the same as this:
describeComponent(CupcakeFlavor, ({ shallowWrapper, setProps }) => {
setProps({ sweetness: 6 saltiness: 2 name: "Salted Caramel" });
const flavor = shallowWrapper({ context: { linerColor: "pink" } });
});
shallowWrapper
is memoized, so it will only call shallow
once per test, and
subsequent calls will return the first ShallowWrapper instance.
shallowWrapper() === shallowWrapper(); // true
This means that you can use shallowWrapper
as many times as you want without
any performance penalty:
expect(shallowWrapper().find(".healthiness").text()).toBe("very bad");
expect(shallowWrapper().find(".tastiness").text()).toBe("very good");
Usually when you use shallowWrapper
, you rename it so that its name matches
the name of the component under test, but written in lowerCamelCase:
describeComponent(PuffyCloud, ({ shallowWrapper: puffyCloud, setProps }) => {
it("renders an img whose src is the cloud image that matches the provided shape", () => {
setProps({ shape: "small puppy" });
expect(puffyCloud().find("img").props().src).toBe(cloudImages["small puppy"]);
});
});
The ShallowWrapper
created by shallowWrapper
will be unmounted automatically
after each test.
renderWrapper
renderWrapper([enzymeOptions]) => CheerioWrapper
A wrapper around Enzyme's render
that will render your component to static markup (using the props set by
setProps
and clearProps
), and return the
CheerioWrapper
created by render
.
// This...
const shades = render(
<AwesomeSunglasses framesColor="black" lensesColor="indigo" />
);
// ... is roughly the same as this:
describeComponent(AwesomeSunglasses, ({ renderWrapper, setProps }) => {
setProps({ framesColor: "black", lensesColor: "indigo" });
const shades = renderWrapper();
});
If present, the options passed into renderWrapper will be passed into Enzyme's
render
as the second argument.
// This...
const shades = render(
<AwesomeSunglasses framesColor="black" lensesColor="indigo" />,
{ context: { insuranceProvider: "Acme Insurance" } }
);
// ... is roughly the same as this:
describeComponent(AwesomeSunglasses, ({ renderWrapper, setProps }) => {
setProps({ framesColor: "black", lensesColor: "indigo" });
const shades = renderWrapper({ context: { insuranceProvider: "Acme Insurance" } });
});
Unlike mountWrapper
and shallowWrapper
,
renderWrapper
is NOT memoized, so it will call render
every time you call
renderWrapper
.
renderWrapper() === renderWrapper(); // false
Usually when you use renderWrapper
, you rename it so that its name matches
the name of the component under test, but written in lowerCamelCase:
describeComponent(RockinGuitar, ({ shallowWrapper: rockinGuitar, setProps }) => {
it("renders an audio element whose src is set based on the tone prop", () => {
setProps({ tone: "squeedly-wow!" });
expect(rockinGuitar().find("audio").props().src).toBe("overdrive.wav");
});
});
setProps
setProps(Object) => undefined
A function which sets the props to render the component with.
You call it with an object whose key/value pairs correspond to prop names and values:
setProps({ wantsShampoo: true, deluxeCut: false });
// The props are now wantsShampoo={true} deluxeCut={false}
Once you've called it, you can then use mountWrapper
,
shallowWrapper
, or renderWrapper
to
render the component.
setProps({ hairstyle: "large and in charge" });
mountWrapper();
If you call it more than once, it does NOT replace the existing props; instead,
it shallowly mixes the props you pass into the existing props, much like a
React Component's setState
method:
setProps({ pedicure: true });
setProps({ manicure: false });
// The props are now pedicure={true} manicure={false}
This behavior is useful when using nested describes to describe different states your component can be in as a result of the props it received:
describe("when `pedicure` is true", () => {
beforeEach(() => setProps({ pedicure: true }));
describe("and `manicure` is false", () => {
beforeEach(() => setProps({ manicure: false }));
it("has an expected appointment duration of 30 minutes", () => {
// Your test goes here
});
});
});
If you want to clear all the props, use the clearProps
function.
When using mountWrapper
or
shallowWrapper
, you may only use setProps
before the
first time you call mountWrapper
/shallowWrapper
. If you try to use it after
the component has rendered, an error will be thrown. If you want change the
props of an already-mounted component, you should use the setProps
method on
the ReactWrapper
/ShallowWrapper
returned from mountWrapper
/shallowWrapper
instead:
// instead of this
mountWrapper();
setProps({ "here's": "some", new: "props" }); // ❌ ERROR!
// do this
mountWrapper();
mountWrapper().setProps({ okay: "but", really: "though" }); // All good! 👍
The reason describe-component
doesn't treat these two forms interchangeably is
that changing the props of an already-rendered component will go through a
different code path (componentWillReceiveProps
) than setting the props for a
component before mounting it (componentWillMount
), so it's important not
to mix up the two.
class EnthusiasticComponent extends React.Component {
componentWillMount() {
console.log("Time to mount! Here's my props:", this.props);
}
componentWillReceiveProps(nextProps) {
console.log("Already mounted! Receiving some new props, too:", nextProps);
}
}
describeComponent(EnthusiasticComponent, ({ mountWrapper: enthusiasticComponent, setProps }) => {
setProps({ ice: "cream" });
enthusiasticComponent(); // Time to mount! Here's my props: { ice: "cream" }
enthusiasticComponent().setProps({ sand: "wich" }); // Already mounted! Receiving some new props, too: { ice: "cream", sand: "wich" }
})
However, when using renderWrapper
, calling setProps
after
rendering is totally fine:
renderWrapper();
setProps({ please: "work" }); // Sure thing! 😀
The decision was made to allow this in this case because a
CheerioWrapper
has some significant differences when compared to a
ReactWrapper
or
ShallowWrapper
:
- A
CheerioWrapper
contains static HTML markup, with no knowledge of React or component lifecycle methods. All your component instances go away once rendering is complete. - Because of this, a
CheerioWrapper
does not have asetProps
method on it.
These differences would make a CheerioWrapper
hard to work with if you were
not allowed to call setProps
after rendering one:
// Okay, I want to verify that this component has the same html when
// I give it the prop colors={["white", "gold"]} as when I give it
// colors={["blue", "black"]}.
describeComponent(ThatDress, ({ renderWrapper: thatDress, setProps }) => {
setProps({ colors: ["white", "gold"] });
const htmlWhenWhiteGold = thatDress().html();
// If setProps wasn't allowed to be called here, there'd be no way to compare
// to new props, because thatDress().setProps() doesn't exist when using
// renderWrapper.
setProps({ colors: ["blue", "black"] });
const htmlWhenBlueBlack = thatDress().html();
expect(htmlWhenWhiteGold).toBe(htmlWhenBlueBlack);
});
clearProps
clearProps() => undefined
A function which will clear all the props set by setProps
.
setProps({ chunky: "bacon" });
// props is now chunky="bacon"
clearProps();
// props is now empty
The same rules apply to clearProps
as they do to setProps
:
namely, you cannot call clearProps
after mountWrapper
or
shallowWrapper
have been called.
props
props() => Object
A function which will return the current props.
setProps({ one: "two" });
props(); // { one: "two" }
setProps({ three: "four" });
props(); // { one: "two", three: "four" }
setProps({ can: "I", have: "a", little: "more" });
props(); // { one: "two", three: "four", can: "I", have: "a", little: "more" }
This can be useful when you want to write an assertion that props got spread onto a rendered component without checking every single prop key and value:
const TheGreatDelegator = (props) => (
<a href="https://www.xkcd.com/1790/" {...props}>
No, YOU deal with this.
</a>
)
describeComponent(TheGreatDelegator, ({ mountWrapper: delegator, setProps }) => {
it("sends all its props right on through to an anchor element", () => {
setProps({
"I've": "got",
"99": "problems",
"but": "a",
"potty": "mouth",
"ain't": "one",
});
const anchor = delegator().find("a");
expect(anchor.props()).toMatchObject(props());
});
});
It can also be useful when you want to verify that a specific prop was threaded through to a rendered component, without saving the value of the prop in a variable:
const GoodLuckClickingThis = ({ onClick }) => (
<div onClick={onClick} />
);
describeComponent(GoodLuckClickingThis, ({ mountWrapper: goodLuck, setProps }) => {
// Instead of this:
describe("when it receives an onClick prop", () => {
let onClick = () => {};
beforeEach(() => {
setProps({ onClick });
});
it("threads its onClick prop down to its rendered div", () => {
const div = goodLuck().find("div");
expect(div.props().onClick).toBe(onClick);
});
});
// You can do this:
describe("when it receives an onClick prop", () => {
beforeEach(() => {
setProps({ onClick: () => {} });
});
it("threads its onClick prop down to its rendered div", () => {
const div = goodLuck().find("div");
expect(div.props().onClick).toBe(props().onClick);
});
});
});
Usage (Test Runner Configuration)
Jest
import describeComponent from "describe-component/jest";
import MyComponent from "./MyComponent";
describeComponent(MyComponent, ({
// It's common to use only one of mountWrapper, shallowWrapper, or
// renderWrapper, and rename the one you use to match the name of
// your component. In this example, we'll use mountWrapper
mountWrapper: myComponent,
// shallowWrapper,
// renderWrapper,
// Helpers that set the props for the component to be rendered with
setProps, // Call with an object to merge into the props
clearProps, // Call to clear the props
props, // Returns the props that the component will be/was rendered with.
}) => {
// Write your tests here
});
See also the Jest example.
Mocha
import describeComponent from "describe-component/mocha";
import MyComponent from "./MyComponent";
describeComponent(MyComponent, ({
// It's common to use only one of mountWrapper, shallowWrapper, or
// renderWrapper, and rename the one you use to match the name of
// your component. In this example, we'll use mountWrapper
mountWrapper: myComponent,
// shallowWrapper,
// renderWrapper,
// Helpers that set the props for the component to be rendered with
setProps, // Call with an object to merge into the props
clearProps, // Call to clear the props
props, // Returns the props that the component will be/was rendered with.
}) => {
// Write your tests here
});
See also the Mocha example.
Jasmine
import describeComponent from "describe-component/jasmine";
import MyComponent from "./MyComponent";
describeComponent(MyComponent, ({
// It's common to use only one of mountWrapper, shallowWrapper, or
// renderWrapper, and rename the one you use to match the name of
// your component. In this example, we'll use mountWrapper
mountWrapper: myComponent,
// shallowWrapper,
// renderWrapper,
// Helpers that set the props for the component to be rendered with
setProps, // Call with an object to merge into the props
clearProps, // Call to clear the props
props, // Returns the props that the component will be/was rendered with.
}) => {
// Write your tests here
});
See also the Jasmine example.
AVA
import test from "ava";
import MyComponent from "./MyComponent";
const describeComponent = require("describe-component/ava")(test);
describeComponent(MyComponent, ({
// It's common to use only one of mountWrapper, shallowWrapper, or
// renderWrapper, and rename the one you use to match the name of
// your component. In this example, we'll use mountWrapper
mountWrapper: myComponent,
// shallowWrapper,
// renderWrapper,
// Helpers that set the props for the component to be rendered with
setProps, // Call with an object to merge into the props
clearProps, // Call to clear the props
props, // Returns the props that the component will be/was rendered with.
}) => {
// Write your tests here
});
See also the AVA example.
Generic/Other
// If your test runner isn't listed or supported yet, you can configure
// describe-component manually to work with it as long as it has support for
// beforeEach/afterEach hooks.
import makeDescribeComponent from "describe-component";
import MyComponent from "./MyComponent";
const describeComponent = makeDescribeComponent({
// A function that works like jest/jasmine/mocha's `describe`; that is, it
// declares a "context" about what you're testing. describeComponent will only
// call it once (passing in the component's name).
describe: global.describe,
// A function that registers the callback it receives to be run before each
// test in the `describe` block, like jest/jasmine/mocha's `beforeEach`.
// describeComponent will only call it once, inside the callback it passes
// to your `describe` function.
beforeEach: global.beforeEach,
// A function that registers the callback it receives to be run after each
// test in the `describe` block, like jest/jasmine/mocha's `afterEach`.
// describeComponent will only call it once, inside the callback it passes
// to your `describe` function.
afterEach: global.afterEach,
// The name of the function a user can use to set up "before each" callbacks.
// This is only used in error messages.
beforeEachName: "beforeEach",
});
// Call describeComponent normally
describeComponent(MyComponent, ...);
See also the way it was done for AVA.
License
MIT