Do you want to request a feature or report a bug?
Feature/Question
Feature: What is your use case for such a feature?
Unit testing of _connected components_ (i.e. using a connector such as connectHits)
Feature: What is your proposed API entry? The new option to add? What is the behavior?
I want to unit test 馃毀 one of my component which is a _connected component_ (in my case with connectHits). I guess I would need to _mock_ the connector, but I haven't found how.
I'm using create-react-app, jest, and enzyme and haven't ejected (and I don't plan on doing so in the near future 馃槈)
My ConnectedFoo component:
import { connectHits } from "react-instantsearch/connectors";
import Foo from "./Foo";
const ConnectedFoo = connectHits(Foo);
export default ConnectedFoo;
My ConnectedFoo test file:
import { shallow } from "enzyme";
import ConnectedFoo from "./ConnectedFoo";
const wrapper = shallow(<ConnectedFoo />);
it("renders a `Foo` component", () => {
expect(wrapper.find(Foo).length).toEqual(1);
})
This obviously fails since connectHits is not mocked and returns an error
TypeError: Cannot read property 'store' of undefined
at new Connector (node_modules/react-instantsearch/src/core/createConnector.js:95:34)
Is there a way to properly _unit test_ connected components?
The most obvious way is to test your component before you connect it, that way you can test your component in all use cases, but it requires you to export the raw component as well. That's what we do in this code base.
Another option is to mock the relevant part of react-instantsearch-dom. A tutorial of that is here.
I tried to find an existing mock for a HOC, but didn't find anything obvious.
I'm gonna try it out in a minute and give my experience
So I've been trying some things, including mocking parts of React InstantSearch, and came to the following suggestions:
(and my preference): export the raw component (not connected).
This is a better approach because you don't need to maintain a mock of each connector you're using, but can rather test your component as if it didn't have anything to do with Algolia or asynchronous data at all. You do this simply by both exporting the ConnectedHits, as well as the original Hits.
mock connectHits for your tests
This is a slightly more involved process. Firstly create /__mocks__/react-instantsearch-dom.js. This file needs to be in the same directory as you also have the node_modules in. In that file you put the following mock for each connector you use in your tests:
const React = require('react');
const MockReactInstantSearch = jest.genMockFromModule(
'react-instantsearch-dom'
);
const fakeHits = [
{ objectID: '1', name: 'bla' },
{ objectID: '2', name: 'blp' },
{ objectID: '3', name: 'bli' },
{ objectID: '4', name: 'bla' },
{ objectID: '5', name: 'bli' },
{ objectID: '6', name: 'blp' },
{ objectID: '7', name: 'bli' },
{ objectID: '8', name: 'bla' },
{ objectID: '9', name: 'bli' },
{ objectID: '10', name: 'blp' }
];
MockReactInstantSearch.connectHits = Component => () => (
<Component hits={fakeHits} />
);
module.exports = MockReactInstantSearch;
You can make this mock however you want to, maybe even configurable with a private method like done in the Jest mocking tutorial.
Next in your actual test file, you can forget about the implementation of connectHits, since it's always going to return the array we provided. Note though that for real interesting tests, you'd want multiple different scenarios (10 hits, no hits, undefined ...). This is a lot easier to construct in the first strategy. In my test I used React Test Renderer, but Enzyme should work just the same (although shallow rendering here won't be a useful test, since it will only test our mock, not the original function). The test I wrote looks like this:
import React from 'react';
import renderer from 'react-test-renderer';
import { ConnectedHits } from '../Hits';
it('does something', () => {
const component = renderer.create(<ConnectedHits />);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
My original Hits.js looked like:
import React from 'react';
import { connectHits } from 'react-instantsearch-dom';
const Hits = ({ hits }) => (
<div>
{hits.map(hit => (
<p key={hit.objectID}>{hit.name}</p>
))}
</div>
);
export const ConnectedHits = connectHits(Hits);
This gives me a following snapshot:
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`does something 1`] = `
<div>
<p>
bla
</p>
<p>
blp
</p>
<p>
bli
</p>
<p>
bla
</p>
<p>
bli
</p>
<p>
blp
</p>
<p>
bli
</p>
<p>
bla
</p>
<p>
bli
</p>
<p>
blp
</p>
</div>
`;
I'll reiterate, the first method requires no setup at all, the second is quite complicated and has clear downsides. This is also why we use the first method in React InstantSearch itself.
Have a nice day, and feel free to comment if you have other questions or open a new issue for other things.
Thanks a lot @Haroenv! Very detailed and quick answer indeed!
I do use approach 1. already - i.e. testing the raw component (not connected). However I maybe overly simplified my question... I do a _little additional logic_ inside ConnectedHits rather than just "connecting" it. That explains why I wanted to test my _raw component_
It was more something like this.
Context:
Foois actually aMapand I'm rendering the markers in the raw component
import { connectHits } from "react-instantsearch/connectors";
import Map from "./Map";
const ConnectedMap = connectHits(({ hits }) => {
// manipulate the hits
const markers = hits.map(hit => (
<Marker
lat={hit._geoloc.lat}
lng={hit._geoloc.lng}
key={hit.objectID}
/>
));
<Map>
{markers}
</Map>
};
export default ConnectedMap;
You are right: testing the connected component is not my responsability, rather the react-instantsearch's.
Hence I think I made a design mistake: the difference between Map and ConnectedMap should be _only the addition of the connector_.
Here is my updated code
import { connectHits } from "react-instantsearch/connectors";
import Map from "./Map";
// Testable with unit tests
const MapWithMarkers = ({ hits }) => {
const markers = hits.map(hit => (
<Marker
lat={hit._geoloc.lat}
lng={hit._geoloc.lng}
key={hit.objectID}
/>
));
<Map>
{markers}
<Map />
};
const ConnectedMap = connectHits(MapWithMarkers);
// MapWithMarkers is only exported for testing purposes
export { MapWithMarkers, ConnectedMap };
MapWithMarkers is taking a hits prop which I can mock (it's just an array), and as such I'm testing an isolated component 馃憤.
The only _drawback_ I see is exporting a component just to test it... but I guess it's a better tradeoff than mocking connectHits.
And tests
import React from "react";
import { shallow } from "enzyme";
import Map from "./Map";
import Marker from "./Marker";
import { MapWithMarkers } from "./ConnectedMap";
const hits = [
{
objectID: "1",
_geoloc: { lat: 42.36, lng: -71.05 }
},
{
objectID: "2",
_geoloc: { lat: -33.86, lng: 151.2 }
}
];
const wrapper = shallow(<MapWithMarkers hits={hits} />);
describe("MapWithMarkers", () => {
it("renders a `Map` component", () => {
expect(wrapper.find(Map).length).toEqual(1);
});
it("renders the as many `Markers` as there are `hits`", () => {
expect(wrapper.find(Marker).length).toEqual(2);
});
});
That qualifies as unit test to me, and solves the issue. Thanks again for pointing this out 馃憤 .
That's great! You're right that exporting it just to be able to test it doesn't seem that great always, but on the other hand, now if you later want to make a map out of items, you're not constrained anymore with necessarily having it in connectHits or even in InstantSearch at all.
Your tests now are easy to follow and don't spend too much time on internal implementation of the connectors, but test the code that you wrote yourself.
Note that if you're using another component within a "raw" component, it might be useful to simply mock it:
jest.mock('react-instantsearch-dom', () => ({
Highlight: ({attribute}) => `Highlighted(${attribute})`,
});
cc @sarahdayan
Most helpful comment
That's great! You're right that exporting it just to be able to test it doesn't seem that great always, but on the other hand, now if you later want to make a map out of items, you're not constrained anymore with necessarily having it in
connectHitsor even in InstantSearch at all.Your tests now are easy to follow and don't spend too much time on internal implementation of the connectors, but test the code that you wrote yourself.