Testing inner logic of method using vue-test-utils with Jest - javascript

How can I test the inner logic of the following method?
For example:
async method () {
this.isLoading = true;
await this.GET_OFFERS();
this.isLoading = false;
this.router.push("/somewhere");
}
So I have the method that toggles isLoading, calls an action, and routes somewhere. How can I be sure that isLoading was toggled correctly (true before action call and false after)?

You have to extract this.isLoading rows into a new method setLoading() and check if it was called.

The second argument of shallowMount/mount is the mounting options that could be used to override the component's data props upon mounting. This lets you pass in a setter that mocks the isLoading data prop, which then allows you to verify the property was modified within the method under test:
it('sets isLoading', () => {
const isLoadingSetter = jest.fn()
const wrapper = shallowMount(MyComponent, {
data() {
return {
// This setter is called for `this.isLoading = value` in the component.
set isLoading(value) {
isLoadingSetter(value)
}
}
}
})
//...
})
Then, you could use toHaveBeenCalledTimes() along with isLoadingSetter.mock.calls[] to examine the arguments of each call to the mocked setter. And since you want to test the effects of the async method, you'll have to await the method call before making any assertions:
it('sets isLoading', async () => {
//...
await wrapper.vm.method()
expect(isLoadingSetter).toHaveBeenCalledTimes(2)
expect(isLoadingSetter.mock.calls.[0][0]).toBe(true)
expect(isLoadingSetter.mock.calls.[1][0]).toBe(false)
})
If you also want to verify that GET_OFFERS() is called, you could use jest.spyOn() on the component's method before mounting:
it('gets offers', async () => {
const getOfferSpy = jest.spyOn(MyComponent.methods, 'GET_OFFERS')
const wrapper = shallowMount(MyComponent, /*...*/)
await wrapper.vm.method()
expect(getOfferSpy).toHaveBeenCalledTimes(1)
})

Related

Vue-test-utils: How do I mock the return of an action in VueX?

I'm writing a test for a Vue component which dispatches to a module store to perform an action and use the result from it.
The action makes a call to our API so I don't want to run the test with that action, but instead mock it and return some dummy data to see that the rest of the method flow works.
So in my test I add a mock store, with a mocked action which just returns hardcoded data, with the intent to see that the component method getData() sets the response of the action to the data.
This doesn't seem to work however and instead it seems as if the real action is called. How do I set it up so that I can avoid calling the real actions and instead use the ones I create for the tests?
Component method, simplified:
methods: {
async getData() {
const response = this.$store.dispatch("global/getDataFromAPI")
if (!response) return
this.$data.data = {...response.data}
}
}
Test code, simplified:
describe('Component.vue', () => {
let localVue;
let vuetify;
let wrapper;
let store;
beforeEach(() => {
localVue = createLocalVue();
localVue.use(Vuex)
vuetify = new Vuetify();
let globalActions = {
getDataFromAPI: async () => {
return {
status: 200,
data: {
information1: "ABC",
information2: 123,
}
}
}
}
store = new Vuex.Store({
modules: {
global: {
actions: globalActions,
namespaced: false
},
}
})
wrapper = mount(Component, {
localVue,
vuetify,
attachTo: div,
mocks: {
$t: () => { },
$store: store,
},
});
});
it('Data is set correctly', async () => {
await wrapper.vm.getData();
const dataInformation1 = wrapper.vm.$data.data.information1;
expect(dataInformation1).toBe("ABC")
});
First, if you want to mock the Vuex Store you don't need to call localVue.use(Vuex). You should call localVue.use(Vuex) only if you are going to use real Vuex Store in test. And if you are going to you must pass store object along with localVue and another arguments, not in mocks property.
Second, to mock your action you can mock store's dispatch method like this:
mocks: {
$store: {
dispatch: () => { dummyData: 'dummyData' }
}
}

A sinon.spy function that is called does not state it has been called

I'm trying to mock an EmberJS adapter which has a function that carries out a POST request. My test looks like this:
test('should post something', async function (assert) {
let controller = this.owner.lookup('path/to/ach-deposit');
controller.setProperties({
...,
store: {
adapterFor: () => {
return {postAchDeposit: sinon.spy()}
}
}
})
await controller.actions.postAchDepositHandler.call(controller);
assert.ok(controller.store.adapterFor.call().postAchDeposit.called);
})
This fails. Stepping into the code of where postAchDeposit is called throws no errors. If I were to change sinon.spy() to sinon.stub().return("Hi") it would return Hi but for whatever reason when I try to see if it has been called, it returns false.
In the debugger after postAchDeposit has been called if I check there using this.get('store').adapterFor('funding/ach-deposit').postAchDeposit.called still it returns false.
What am I missing?
each time adapterFor is called sinon.spy() is called again and so a new spy is created.
So essentialls:
controller.store.adapterFor().postAchDeposit !== controller.store.adapterFor().postAchDeposit
You probably should first create your stub and then always pass a reference:
const postAchDeposit = sinon.spy();
controller.setProperties({
...,
store: {
adapterFor: () => {
return {postAchDeposit}
}
}
});
await controller.actions.postAchDepositHandler.call(controller);
assert.ok(postAchDeposit.called);

How to add this in a function to callback of handleChange and componentDidMount?

How to add this in a function to callback of handleChange and componentDidMount?
// callback from github api /api.github.com/repos/{user}/{repo}
const [repository, issues] = await Promise.all([
api.get(`/repos/${repoName}`),
api.get(`/repos/${repoName}/issues`, {
params: {
state: 'all',
// I need change this by select options = all, closed and open (per example)
per_page: 5,
},
}),
]);
// this define setState again in handleChange
this.setState({repository: repository.data,
issues: issues.data,
loading: false,
});
// Example:
componentDidMount () {functionUP();};
handleChange () {functionUpAgain(with state from params = all, open or closed);
};
well since you are using await your function needs to be async.
So something like
class YourComponent extends React.Component {
// callback from github api /api.github.com/repos/{user}/{repo}
async fetchData(state = 'all') {
const [repository, issues] = await Promise.all([
api.get(`/repos/${repoName}`),
api.get(`/repos/${repoName}/issues`, {
params: {
state,
// I need change this by select options = all, closed and open (per example)
per_page: 5,
},
}),
]);
// this define setState again in handleChange
this.setState({
repository: repository.data,
issues: issues.data,
loading: false,
});
}
// Example:
componentDidMount() {
this.fetchData();
};
handleChange = () => {
const somestate = 'open'; // here you decide what state to pass to the fetchData
this.fetchData(somestate);
};
render() {
}
}
I cannot help with how to get the state to pass the fetchData since your code does not show where that might come from. But the example code allows to pass a parameter to the fetchData method which will passed to the API.
You also should consider refactoring that code so that api.get(/repos/${repoName}) is not called always (since it looks like it uses no params and it would return the same data always)

this.setState not working on componentWillMount()?

I want the state to be dependent on server data. I thought of using componentWillMount:
componentWillMount() {
this.setState( async ({getPublicTodosLength}, props) => {
const result = await this.getPublicTodosLengthForPagination();
console.log("result = ", result) // returns the length but not assigned on this.state.getPublicTodosLength
return { getPublicTodosLength: result+getPublicTodosLength }
});
}
getPublicTodosLengthForPagination = async () => { // get publicTodos length since we cannot get it declared on createPaginationContainer
const getPublicTodosLengthQueryText = `
query TodoListHomeQuery {# filename+Query
viewer {
publicTodos {
edges {
node {
id
}
}
}
}
}`
const getPublicTodosLengthQuery = { text: getPublicTodosLengthQueryText }
const result = await this.props.relay.environment._network.fetch(getPublicTodosLengthQuery, {})
return await result.data.viewer.publicTodos.edges.length;
}
There is value but it's not assigned on my getPublicTodosLength state? I think I don't have to bind here since result returns the data I wanted to assign on getPublicTodosLength state
Why not rather do something like this?
...
async componentWillMount() {
const getPublicTodosLength = this.state.getPublicTodosLength;
const result = await this.getPublicTodosLengthForPagination();
this.setState({
getPublicTodosLength: result+getPublicTodosLength,
});
}
...
It's simpler and easier to read. I think the problem with the original code is with using async function inside setState(). In transpiled code there is another wrapper function created and then it probably loose context.
If you want your state to be dependent on server data you should use componentDidMount().
componentWillMount() is invoked immediately before mounting occurs. It is called before render(), therefore setting state synchronously in this method will not trigger a re-rendering. Avoid introducing any side-effects or subscriptions in this method.
This is the only lifecycle hook called on server rendering. Generally, we recommend using the constructor() instead.
componentDidMount() is invoked immediately after a component is mounted. Initialization that requires DOM nodes should go here. If you need to load data from a remote endpoint, this is a good place to instantiate the network request. Setting state in this method will trigger a re-rendering.
From React Doc
May be you could use:
Code snippet:
(async ({getPublicTodosLength}, props) => {
const result = await this.getPublicTodosLengthForPagination();
console.log("result = ", result);
return { getPublicTodosLength: result + getPublicTodosLength }
})).then((v)=> this.setState(v));
Please let me know if that works.
i decided to make componentWillMount async and it worked well.
this is the code:
componentWillMount = async () => {
let result = await this.getPublicTodosLengthForPagination();
this.setState((prevState, props) => {
return {
getPublicTodosLength: result
}
});
}

React Jest Test Fails when setState Called in Promise

I'm trying to mock out the a service that returns promises so that I can verify it gets called with the correct parameters. The way the service is called varies based on the state and the first call to the service sets the state.
When setting the state in the promise it is not updating unless I wrap the assertion in setTimeout or completely stub out the promise. Is there a way to do this with just a plain promise and an expect?
My component:
class App extends Component {
constructor(props) {
super(props);
this.state = {results: []};
this.service = props.service;
this.load = this.load.bind(this);
}
load() {
if (this.state.results.length === 0) {
this.service.load('state is empty')
.then(result => this.setState({results: result.data}));
} else {
this.service.load('state is nonempty')
.then(result => this.setState({results: result.data}));
}
}
render() {
return (
<div className="App">
<button id="submit" onClick={this.load}/>
</div>
);
}
}
My test:
it('Calls service differently based on results', () => {
const mockLoad = jest.fn((text) => {
return new Promise((resolve, reject) => {
resolve({data: [1, 2]});
});
});
const serviceStub = {load: mockLoad};
let component = mount(<App service={serviceStub}/>);
let button = component.find("#submit");
button.simulate('click');
expect(mockLoad).toBeCalledWith('state is empty');
button.simulate('click');
//this assertion fails as the state has not updated and is still 'state is empty'
expect(mockLoad).toBeCalledWith('state is nonempty');
});
As mentioned, the following works, but I'd rather not wrap the expect if there's a way around it:
setTimeout(() => {
expect(mockLoad).toBeCalledWith('state is nonempty');
done();
}, 50);
I can also change how I mock the function to stub out the promise which will work:
const mockLoad = jest.fn((text) => {
return {
then: function (callback) {
return callback({
data : [1, 2]
})
}
}
});
But I'd like to just return a promise.
React batches setState calls for performance reasons, so at this point
expect(mockLoad).toBeCalledWith('state is nonempty');
the condition
if (this.state.results.length === 0) {
is most likely still true, because data has not yet been added to state.
Your best bets here are
Either use forceUpdate between the first and second click event.
Or split the test into two separate, while extracting common logic outside of the test. Even the it clause will become more descriptive, for instance: it('calls service correctly when state is empty') for the first test, and similar for the second one.
I'd favour the second approach.
setState() does not always immediately update the component. It may batch or defer the update until later.
Read more here.
Using Sinon with Sinon Stub Promise I was able to get this to work. The stub promise library removes the async aspects of the promise, which means that state gets updated in time for the render:
const sinon = require('sinon');
const sinonStubPromise = require('sinon-stub-promise');
sinonStubPromise(sinon);
it('Calls service differently based on results', () => {
const mockLoad = jest.fn((text) => {
return sinon.stub().returnsPromise().resolves({data: [1, 2]})();
});
const serviceStub = {load: mockLoad};
let component = mount(<App service={serviceStub}/>);
let button = component.find("#submit");
button.simulate('click');
expect(mockLoad).toBeCalledWith('state is empty');
button.simulate('click');
expect(mockLoad).toBeCalledWith('state is nonempty');
});
See:
http://sinonjs.org/
https://github.com/substantial/sinon-stub-promise

Categories

Resources