The final step in our testing will be to test the Pointer
class. This is again
similar to the component testing we have been doing in the previous steps.
We will be testing the unconnected version of the component, the connected
version, testing that clicking the controls dispatches the correct Redux
actions, and also testing that clicking the open/close control toggles the
Pointer
components internal "open" state
.
Add the usual imports and describe
contexts for the spec which will we will
place at app/js/components/Pointer/__specs__/Pointer.spec.jsx
.
We will define some mock data that will be used in the unconnected snapshot test
which allows us to define noop
functions for the prop functions that Redux
would normally create for us.
// app/js/components/Pointer/__specs__/Pointer.spec.jsx
import React from 'react';
import { shallow } from 'enzyme';
import { Pointer } from '../';
const noop = () => {};
const pointerProps = {
addFavourite: noop,
removeFavourite: noop,
id: 42,
x: 99,
y: 88,
details: {
name: 'Winterfell',
house: 'Stark',
words: 'Winter is Coming'
},
favourite: true
};
describe('Pointer component', () => {
it('matches the snapshot', () => {
const wrapper = shallow(<Pointer {...pointerProps} />);
expect(wrapper).toMatchSnapshot();
});
});
Next we will import the configureStore
method, the pointsMock
, and check
that the connected Pointer
matches the snapshot.
We also have to stub the x
, y
, and details
props that are expected by our
propTypes
with fake data because Redux does not pass these props to the
Pointer
component for us but instead our Map
component does. Remember that
the Pointer
component does not have a mapStateToProps
function.
// app/js/components/Pointer/__specs__/Pointer.spec.jsx
import React from 'react';
import { shallow } from 'enzyme';
+ import configureStore from 'redux-mock-store';
+ import { pointsMock } from '../../../spec-helper';
- import { Pointer } from '../';
+ import ConnectedPointer, { Pointer } from '../';
const noop = () => {};
const pointerProps = {
addFavourite: noop,
removeFavourite: noop,
id: 'point-42',
x: 99,
y: 88,
details: {
name: 'Winterfell',
house: 'Stark',
words: 'Winter is Coming'
},
favourite: true
};
describe('Pointer', () => {
it('matches the snapshot', () => {
const wrapper = shallow(<Pointer {...pointerProps} />);
expect(wrapper).toMatchSnapshot();
});
});
+ describe('ConnectedApp', () => {
+ const mockStore = configureStore([]);
+ const store = mockStore({ points: pointsMock });
+
+ it('maps store state to the props', () => {
+ const wrapper = shallow(
+ <ConnectedPointer store={store} x={1} y={2} details={{}} />
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ });
+ });
We will now test that clicking on the "favourite" toggle control will dispatch
the addFavourite
or removeFavourite
actions for us depending on the state of
our store.
We also introduce a beforeEach
function that will clear the mock store of any
actions that were dispatched in a previous test, otherwise the actions stay in
the store and our expectations will not match.
The one thing we need to change in the actual Pointer
component is the <a>
tag that we use for the "favourite" and "open/close" controls.
In order to target them in our tests we cannot simply search for "a" because
there are two of them. Instead we will update the href
attribute to add an
identifier after the hash #
. Update the Pointer
component as so:
// app/js/components/Pointer/Pointer.jsx
<header className={styles.headline}>
<h3>{name}</h3>
<div className={styles.detailsControls}>
- <a href="#" className={styles.control} onClick={this.favourite}>
+ <a href="#favourite" className={styles.control} onClick={this.favourite}>
{favourite ? '–' : '+'}
</a>
- <a href="#" className={styles.control} onClick={this.toggle}>
+ <a href="#toggle" className={styles.control} onClick={this.toggle}>
×
</a>
</header>
We can now go ahead and implement the changes to our test.
// app/js/components/Pointer/__specs__/Pointer.spec.jsx
import React from 'react';
- import { shallow } from 'enzyme';
+ import { shallow, mount } from 'enzyme';
import configureStore from 'redux-mock-store';
import { pointsMock } from '../../../spec-helper';
+ import { addFavourite, removeFavourite } from '../../../actions';
import ConnectedPointer, { Pointer } from '../';
...
describe('ConnectedPointer component', () => {
const mockStore = configureStore([]);
const store = mockStore({ points: pointsMock });
+ beforeEach(() => {
+ store.clearActions();
+ });
+
it('maps store state to the props', () => {
const wrapper = shallow(
<ConnectedPointer store={store} x={1} y={2} details={{}} />
);
expect(wrapper).toMatchSnapshot();
});
+ describe('when the favourite button is clicked', () => {
+ it('calls the removeFavourite action if the pointer is a favourite', () => {
+ const wrapper = mount(
+ <ConnectedPointer {...pointerProps} store={store} />
+ );
+ const expectedAction = [removeFavourite(42)];
+
+ wrapper.find('a[href="#favourite"]').simulate('click');
+
+ expect(store.getActions()).toEqual(expectedAction);
+ });
+
+ it('calls the addFavourite action if the pointer is a favourite', () => {
+ const updatedFavouriteState = { favourite: false };
+ const modifiedPointerProps = Object.assign(
+ {},
+ pointerProps,
+ updatedFavouriteState
+ );
+
+ const wrapper = mount(
+ <ConnectedPointer {...modifiedPointerProps} store={store} />
+ );
+ const expectedAction = [addFavourite(42)];
+
+ wrapper.find('a[href="#favourite"]').simulate('click');
+
+ expect(store.getActions()).toEqual(expectedAction);
+ });
+ });
+ });
The final step will be to test that the state
property updates when the
open/close toggle control is clicked on.
Since we do not need Redux to test this, these tests will be added inside the
unconnected Pointer
context.
These tests are similar to the action tests we did previously. We mount the
component using mount
, find the button we want to click on the rendered DOM,
and then simulate a click on it.
In this case though we will expect that the component state
updates instead of
a Redux action being dispatched.
We can mock the state
of the component by calling .state()
on the wrapper
and passing an object of the state to be set, as you see in the "changes open
state to true, if it is false" test.
We can also get the current state
of the component by asking for one of the
state keys, for example wrapper.state('open');
.
We use this to form our expectations in each test scenario. Add the following tests to the "Pointer component" context.
// app/js/components/Pointer/__specs__/Pointer.spec.jsx
describe('Pointer component', () => {
it('matches the snapshot', () => {
const wrapper = shallow(<Pointer {...pointerProps} />);
expect(wrapper).toMatchSnapshot();
});
+ describe('when the toggle button is clicked', () => {
+ it('changes the open state to true, if it is false', () => {
+ const wrapper = mount(<Pointer {...pointerProps} />);
+
+ wrapper.find('a[href="#toggle"]').simulate('click');
+
+ expect(wrapper.state('open')).toEqual(true);
+ });
+
+ it('changes the open state to false, if it is true', () => {
+ const wrapper = mount(<Pointer {...pointerProps} />);
+
+ wrapper.setState({ open: true });
+ wrapper.find('a[href="#toggle"]').simulate('click');
+
+ expect(wrapper.state('open')).toEqual(false);
+ });
+ });
});
That's everything! We have now tested all of our components. If we update our components in a breaking way in the future the tests will fail and we will have to update them to match the new state of the code, or if they are failing unexpectedly we will know that a bug has been introduced to our application.
Run the tests once more with yarn test
and see that everything including our
Redux actions and reducer are covered with passing specs.