Need: notes on Shallow, Mount, and wrapper.instance()
import React from 'react'; import { shallow } from 'enzyme'; import { Provider } from 'react-redux'; import { getDataFetch } from '../../util/apiCalls.js'; import { setActions } from '../../actions/index.js'; import { App, mapStateToProps, mapDispatchToProps } from './App';
` describe('Component', () => { let mockData; let wrapper;
beforeEach(() => {
mocked data
}
}`
You'll want to mock out your props here. They should be a shorter, easier version of the data you're using in your app.
You also need to mock out your API call here, otherwise it will mess up the rest of your tests. You just use mockImplementation to make it return what you want it to return, which is a Promise that resolves to the data that you're pulling in from the fetch.
Needs to be before you define your shallow/wrapper because that mounts the component, and you don't want to do that before you do your fetch.
This is what a mock API call looks like:
getInfo.mockImplementation(() => { return Promise.resolve(mockData) })
(Sometimes your API request will be used for more than one thing - in that case, have it the Promise resolve a mockArgument
, and at the beginning of your test back assign mock argument to the relevant data. mockArgument = mockData
)
This is the format you'll use for unit tests - actually mocking out an API call for API testing has a lot more to it.
A snapshot is getting a picture of how something will render. If the render depends on certain props, take a snap for each prop. Here is the syntax:
`const wrapper = shallow()
it('should render App with correct data', () => { expect(wrapper).toMatchSnapshot() })`
When you test that a function was called, you're using an enzyme method called .toHaveBeenCalled()
or .toHaveBeenCalledWith
. These functions only work on functions that have been mocked out. Luckily, you already mocked out your functional props with jest.fn()
when you made your wrapper component, so all you have to do is check whether the WRAPPED prop was called. Here is the syntax:
` it('should call myFunction when parentFunction is called', () => { wrapper.instance().supplyHosts()
expect(wrapper.instance().props.storeHosts).toHaveBeenCalledWith(mockHosts)
})`
wrapper.instance().props.storeHosts is how you "name" a prop function in a test that's calling that prop function off the wrapper.
.toHaveBeenCalled
still expects a mocked out function, so if it's a class method, you need to mock that out too. That has not been mocked out in wrapper, so you need to do it in the test. Simply call the function off the instance of the wrapper, and reassign it to jest.fn()
Here is an example when the parent function is componentDidMount:
`it('should call myFunction when component mounts', () => { wrapper.instance().myFunction = jest.fn()
wrapper.instance().componentDidMount()
expect(wrapper.instance().myFunction).toHaveBeenCalled()
})`
Sometimes, you need to check that an async function is working correctly, but enzyme and jest have no idea what a fetch is, so the test explodes when it gets to the async function.
parentAsyncFunction = () => { return getData('url.stuff.enpoint', 'type') .then(data => this.props.myFunction(data)) }
Above in the beforeEach()
section you can see how to mock out an async function. But you still need to make the TEST async if you want to test an async function.
Make sure to put the await in the right spot! It needs to be on the function that needs to finish running before you can run anything else.
`it('should call getData and myFunction when parentAsyncFunction is called', async () => { await wrapper.instance().parentAsyncFunction()
expect(wrapper.instance().props.myFunction).toHaveBeenCalledWith(mockData)
expect(getInfo).toHaveBeenCalled()
})Note, here
getData` was imported and mocked out in the beforeEach.
Testing that a function was called when that function's value is something the parent function needs to run
`parentFunction = () => { const neededData = this.myFunction() const getTheData = neededData.includes(this.props.function.id)
return getTheData
}`
This is another example of where mocks and spies come in handy, because otherwise the function won't have anything to call .includes off of. .mockImplementation actually removes the entire function from the component and replaces it with another. Spies leaves it in place, and it will run by default with the normal output if it can, but it will take the spie's output if it's there (not sure if it only takes the Spy if it CAN'T access its normal output).
You need to assign the function to a jest.fn()
, but then you also need to make it so that that jest function returns what you need.
// wrapper.instance().myFunction = jest.fn().mockImplementation(() => { // return ( // { // id: objectID infoList: [1,2,3,4] // } // ) // }) // wrapper.instance().forceUpdate() // wrapper.instance().parentFunction() // // expect(wrapper.instance().myFunction).toHaveBeenCalled()
The method for that in Enzyme is expect(wrapper.instance().myFunction).toHaveBeenCalledTimes(3)
Here you basically just need to get your mock data in a good place, make sure your wrapper props are where they need to be, and then make sure the function output is what you expect.
Sometimes you might need to mock out a function if there's one in the function you're testing whose value matters for the eventual value. Here is an example with spies in it.
it('should return what I expect when I call myFunction', () => { jest.spyOn(wrapper.instance(), 'neededFunctionValue').mockImplementation(() => { return ( { id: 9, dataList: [1,2,3,4] } ) }) expect(wrapper.instance().myFunction()).toEqual(the value you expect it to) })
Don't forget, if you have arguments that aren't in your beforeEach, you need to mock those out too!
Usually you see this in functions like this, and the run on render:
generateChildComponents = () => { return this.props.infoList.map(info => { return ( <ChildComponent key={host.id} info={info.payload} id={info.id} /> ) }) }
A separate function is needed to generate an array of child components. It's a little hard to test - you sort of need to make sure that the component display name appears the number of times it should.
it('should have the correct number of host components when it renders', () => { (You might have to call shallow wrapper again here with the correct props) expect(wrapper.find('ChildComponent')).toHaveLength(3) })
Note, if your child component is wrapped, you need to look for it as Connect(ChildComponent)
expect(wrapper.find('Connect(ChildComponent)')).toHaveLength(3) expect(wrapper.find({prop1: '1stValue'})).toHaveLength(1) expect(wrapper.find({prop1: '2ndValue'})).toHaveLength(1) expect(wrapper.find({prop1: '3rdValue'})).toHaveLength(1)
You can also check that each component got the correct prop value!
You can do this pretty much the same way that you do in a funtion. You call setState({key: newValue}) on instance.wrapper() to set the state, and then you use dot notation to call values on it. Here is an example of testing something that toggles state:
`it('should toggle the state to true and false', () => { wrapper.instance().setState({boolean: false}) wrapper.instance().toggleBoolean() expect(wrapper.instance().state.boolean).toEqual(true)
wrapper.instance().setState({boolean: true})
wrapper.instance().toggleBoolean()
expect(wrapper.instance().state.boolean).toEqual(false)
})`
Sometimes a function gets called by a click or change on the DOM. You test this by using the .find method and the .simulate method. Please note, simulate only works on one node!
If that click triggers and event object whose info you need, you will need to mock that out as an argument. An event object is an object, and one of its keys is target. Target has a value of another object - put in the key value pairs you care about.
This might be an event object taken from an input with a name and a value:
mockEvent = { target: { name: 'inputName', value: 'thing I typed' }, preventDefault: jest.fn() }
Throwing in the prevent default is super helpful!
Here is an example of a function we'd need to test using these methods:
handleClick = (event) => { const objectChoice = this.props.objectsList.find(object => { return object.name === event.target.id }) this.props.chooseObject(objectChoice) }
And here is how we'd test it:
` it('should call chooseObject when handleClick is run', () => { const mockEvent = {target: {id: 'funnyName'}} const expected = { id: 3, name: "funnyName", }
wrapper.instance().handleClick(mockEvent)
expect(wrapper.instance().props.chooseObject).toHaveBeenCalledWith(expected)
})`
A reducer test needs to look at two things: Whether it returns its current state when it doesn't know the action (this will be the default state for testing purposes), and whether it returns the action payload(or the correct derivitive of it) when it gets an action type is does know. So for this reducer -
export const infoListReducer = (state = [], action) => { switch (action.type) { case 'SET_INFO': return action.infoList; default: return state; } }
- here is how we'd test that it defaults correctly:
it('should return the initial state', () => { expect(infoListReducer([], {type: undefined, infoList: [stuff, moreStuff])).toEqual([]) })
And here is how we test whether it returns the correct info from the action payload when it gets a type it does like:
`it('should return the correct state with action type SET_INFO', () => { const mockInfoList = [{ id: 1, name: "stuff", }, { id: 2, name: "more stuff", }]
const action = {type: 'SET_INFO', infoList: mockInfoList}
expect(infoListReducer([], action)).toEqual(mockInfoList)
})`
An action creator takes some information and then turns it into a simple object with a type: key value pair (as well as the info, usually). What you're testing when you test an action is whether, when given some info as an argument, it's action creator returns and object with the correct type along with that argument.
Here is an action creator:
export const setInfo = (info) => ({ type: 'SET_INFO', info: info });
And here is a quick test for it:
`it('should have a type SET_INFO', () => { const info = [{}, {} ,{}]
const expected = {type: 'SET_INFO', info: info}
expect(actions.setInfo(info)).toEqual(expected)
})`
What you're testing with mapState is whether the portion of the global store that is brought into your component is correct. mapStateToProps(state)
is kind of a representative of your components own props. So we want to make sure that that is equal to the object that your props are equal to.
Say your global store looks like this (it actually has reducers, but we'll pretend the values are what the reducers return)
{ infoName: 'string', chosenItem: {thing}, categories: [{thing}, {stuff}, {items}] }
Your state, however, is only interested in chosenItem
. The way you'd test this is by importing mapStateToProps from your component and making sure it only looks at the correct key value pair from the global store. This is why you should really mock out your global store to look like your real store as much as possible.
` describe('mapStateToProps', () => { it('should return the correct data from the state', () => { const mockState = { infoName: 'string', chosenItem: {thing}, categories: [{thing}, {stuff}, {items}] }
const expected = {
chosenItem: {thing}
}
expect(mapStateToProps(mockState)).toEqual(expected)
})
})`
What needs to be tested with mapDispatch is whether the dispatch function was called when the prop that's supposed to call it runs. mapDispatchToProps(mockDispatch) is going to sort of be our props object for that component, so you can use dot notation to call a prop. We want to make sure the function it's called with gets run, WITH the action that it should be running with.
You still need to make dispatch into a mock and give it a variable - this is ok because we're basically just making sure that if we feed mapDispatch a function as an argument, that function will get called. And if we call the right prop, it will call the associated action creator.
it('calls dispatch with setInfo when storeInfo is called', () => { const mockDispatch = jest.fn() mapDispatchToProps(mockDispatch).storInfo([{}, {}]) expect(mockDispatch).toHaveBeenCalledWith(setInfo([{}, {}])) })
setInfo here is an action called in from your actions page, and storeInfo is your prop.
Each call gets its own describe block.
In a beforeEach, mock out the data you'll be pulling in/pushing up.
Mock out your options object.
You also need to mock out window.fetch - essentially, it's a promise that returns an object that contains another promise that returns your data.
window.fetch = jest.fn().mockImplementation(() => { return Promise.resolve({ ok: true, json: () => { return Promise.resolve(mockData); }, }); });
You will need to test to make sure that the correct URL gets passed into the api function and that Fetch calls it You will need to make sure it's spitting out the info in the Promise.resolve You will need to make sure it handles the bad path.
Here is how to do the 3 tests for those.
it('should be passed down the correct URL', () => { getData('https://url.com/api/v1/items'); expect(window.fetch).toHaveBeenCalledWith('https://url.com/api/v1/items'); });
it('should return the correct data in the correct format', () => { expect(getData('https://url.com/api/v1/items')).resolves.toEqual(mockData) })
it('should not return data when the response is not ok', () => { window.fetch = jest.fn().mockImplementation(() => { return Promise.resolve({ ok: false, json: () => { return Promise.resolve(mockData) } }) }) expect(getMovieData(''https://url.com/api/v1/items'')).rejects.toEqual(Error('Error fetching movies')) })
The promise might resolve to something else - maybe a string saying, "oh no! It didn't work." Check the API documentation
Same deal except you NEED to mock out options and make sure you know what the promise returns. Usually, it returns some formatted version of what you put in. So if you added a user argument to your fetch for a login, it would return some form of that.
mockOptions = { method: 'POST', body: JSON.stringify(mockUser), headers: { 'Content-Type': 'application/json', }, };
Same deal, different options to mock out and probably a different response. Look at your API!
mockDeleteOptions = { method: 'DELETE', headers: { 'Content-Type': 'application/json' } };
console.log(wrapper.debug())
Are your props given the right value for what you need to be testing right now?
You might need to put a wrapper.instance().forceUpdate() in there if you are reassigning values.
DID YOU RETURN??