I am trying to test an async function in a react native app.
class myClass extends React.Component {
...
closeModal = async () => {
if (someCondition) {
await myFunction1();
} else {
await myFunction2();
}
this.props.navigation.state.params.onGoBack();
this.props.navigation.navigate('Main');
};
...
}
This is my test:
const navigation = {
navigate: jest.fn(),
state: { params: { onGoBack: jest.fn() } },
};
const renderComponent = overrides => {
props = {
navigation,
...overrides,
};
return shallow(< myClass.wrappedComponent {...props} />);
};
describe('When the user presses the close icon', () => {
it('should close the modal', () => {
const wrapper = renderComponent();
const instance = wrapper.instance();
const spyCloseModal = jest.spyOn(instance, 'closeModal');
instance().forceUpdate();
component
.find({ testID: 'close-icon' })
.props()
.onPress();
expect(spyCloseModal).toHaveBeenCalled(); // this is passed
expect(navigation.navigate).toHaveBeenCalled(); // this is not passed
});
});
It looks like it gets stuck on the await calls. If I remove the await calls then it passes. Someone mentioned in another post to use .and.callThrough after spyOn but it gives me this error
Cannot read property 'callThrough' of undefined
one of solution is to make your test async and run await (anything) to split your test into several microtasks:
it('should close the modal', async () => {
const wrapper = renderComponent();
component
.find({ testID: 'close-icon' })
.props()
.onPress();
await Promise.resolve();
expect(navigation.state.params.onGoBack).toHaveBeenCalled();
expect(navigation.navigate).toHaveBeenCalledWith("Main");
});
I believe you don't need either .forceUpdate nor .spyOn on instance method. once navigation happens properly it does not matter by what internal method it has been called
more on microtask vs macrotask: https://abc.danch.me/microtasks-macrotasks-more-on-the-event-loop-881557d7af6f
alternative is to use macrotask(setTimeout(...., 0))
it('should close the modal', (done) => {
const wrapper = renderComponent();
component
.find({ testID: 'close-icon' })
.props()
.onPress();
setTimeout(() => {
expect(navigation.state.params.onGoBack).toHaveBeenCalled();
expect(navigation.navigate).toHaveBeenCalledWith("Main");
done();
});
}
Yes, you're on the right track...the issue is that closeModal is asynchronous.
The await hasn't finished by the time execution returns to the test so this.props.navigation.navigate hasn't been called yet.
The test needs to wait for closeModal to complete before asserting that navigate has been called.
closeModal is an async function so it will return a Promise...
...and you can use the spy to retrieve the Promise it returns...
...then you can call await on that Promise in your test to make sure closeModal has completed before asserting that navigate has been called.
Here is a simplified working example to get you started:
import * as React from 'react';
import { shallow } from 'enzyme';
class MyClass extends React.Component {
closeModal = async () => {
await Promise.resolve();
this.props.navigation.navigate('Main');
}
render() { return <div onClick={() => this.closeModal()}></div> }
}
test('MyClass', async () => { // <= async test function
const props = { navigation: { navigate: jest.fn() }};
const wrapper = shallow(<MyClass {...props} />);
const instance = wrapper.instance();
const spyCloseModal = jest.spyOn(instance, 'closeModal');
wrapper.find('div').simulate('click');
expect(spyCloseModal).toHaveBeenCalled(); // Success!
const promise = spyCloseModal.mock.results[0].value; // <= get the Promise returned by closeModal
await promise; // <= await the Promise
expect(props.navigation.navigate).toHaveBeenCalled(); // Success!
})
Note the use of mockFn.mock.results to get the Promise returned by closeModal.
Related
I have this very simple function, and I must write a test. The goal is to fulfill the coverage threshold.
import { lambdaPromise } from '#helpers';
export const main = async event => lambdaPromise(event, findUsers);
The lambdaPromise() function returns a Promise. I am trying to mock it, then tell if it was called. Here's what I have:
import { main, findUsers } from '../src/lambdas/user/findUsers';
import { lambdaPromise } from '#helpers';
const mockEvent = {
arguments: {
userDataQuery: {
email: 'johndoe#whatever.com'
}
}
};
const mockLambdaPromise = jest.fn();
jest.mock('#helpers', () => ({
lambdaPromise: jest.fn().mockImplementation(() => mockLambdaPromise)
}));
describe('findUsers', () => {
it('should have a main function', async () => {
const mockPromise = main(mockEvent);
expect(mockPromise).toBeInstanceOf(Promise);
expect(mockLambdaPromise).toBeCalledWith(mockEvent, findUsers);
});
});
Now mockLambdaPromise never gets called. How to fix that?
Your mock returns a function, but you didn't call that function. The following makes it pass.
jest.mock("./helpers", () => ({
lambdaPromise: jest
.fn()
.mockImplementation((a, b) => mockLambdaPromise(a, b)),
}));
The complexity of that mock can be reduced by just mocking the resolved value with a spy:
import { main, findUsers } from "./findUsers";
import * as helpers from "./helpers";
describe("findUsers", () => {
it("should have a main function", async () => {
const spy = jest.spyOn(helpers, "lambdaPromise").mockResolvedValue();
await main(mockEvent);
expect(spy).toBeCalledWith(mockEvent, findUsers);
});
});
I'm trying to test whether an async function (fire and forget) gets called.
Content.js
export async function fireAndForgetFunction() {
...
}
export async function getData() {
...
fireAndForgetFunction()
return true;
}
I would like to test if fireAndForgetFunction has been called more than once.
Current test
import * as ContentFetch from '../Content';
const { getData } = ContentFetch;
const { fireAndForgetFunction } = ContentFetch;
it('test',async () => {
const spy = jest.spyOn(ContentFetch, 'fireAndForgetFunction');
await getData();
expect(spy).toHaveBeenCalled();
})
The test result to an error saying
Expected number of calls: >= 1
Received number of calls: 0
How could I do this test?
If you don't want to await for fireAndForgetFunction in getData(), which I assume is the case, then providing a mock implementation of fireAndForgetFunction when creating the spy is your best option:
it('test', (done) => {
const spy = jest.spyOn(ContentFetch, 'fireAndForgetFunction')
.mockImplementation(() => {
expect(spy).toHaveBeenCalled();
done();
})
getData();
})
I have a component which does some SQLite loads in it's componentDidMount() function
async componentDidMount() {
try {
const user = await this.userDao.getUserData();
const setupNeeded = user === null;
if( setupNeeded ) {
this.setState({
status : 'setup'
});
}
else {
this.setState({
status : 'ready',
seed: user.seed
})
}
} catch (e) {
logger.error("Error during db query", e);
this.setState({
status: 'corrupted'
});
}
}
And I would like to test the rendered result after the call to getUserData() has been resolved, and the state has been set accordingly.
Now in my test, I have the actual call to the database mocked away, so the Promise should be resolved immediately, yet testing like this does not work as expected:
Initially, I tried it like this:
test('Should render SetNewPasswordScreen', async () => {
const tree = await renderer.create(<IndexScreen/>);
const json = tree.toJSON();
// expect stuff
});
However the json in this case contains the data of the initial render() call.`
Doing it like this will work:
test('Should render SetNewPasswordScreen', (done) => {
const tree = renderer.create(<IndexScreen/>);
setTimeout(() => {
const json = tree.toJSON();
// expect stuff
}, 5000);
});
But this is not ideal, because I am just guessing that after 5 seconds everything will be done, but I don't know. Also, it's less then suitable if a test takes 5 seconds to finish. (I used 5 seconds arbitrarily, probably it will also work with much less since the async call is mocked anyway, but I can never really know)
My question is if anybody has a better idea of how to solve this issue?
it('should render setNewPassWordScreen', async () => {
const tree = await renderer.create(<IndexScreen/>);
const instance = tree.getInstance();
await instance.componentDidMount();
// expect other stuff.
});
Little late for the party but faced the issues as well. Here is a handy shortcut function I use with the adviced usage of act:
import renderer from "react-test-renderer"
import { ReactElement } from "react"
import { act } from "#testing-library/react-native"
export const expectToMatchSnapshot = async (component: ReactElement) => {
let tree
await act(async () => {
tree = renderer.create(component)
})
expect(tree.toJSON()).toMatchSnapshot()
}
In your case you should only then add:
test("should render ***", async () => {
await expectToMatchSnapshot(<IndexScreen/>)
}
you could use the wait for expect package:
https://www.npmjs.com/package/wait-for-expect
test('Should render SetNewPasswordScreen', async (done) => {
const tree = renderer.create(<IndexScreen/>);
await waitForExpect(() => {
const json = tree.toJSON();
// expect stuff
});
});
mind the function passed into the test('...', fn) is now an async function.
I'm trying test for cases when my axios call does not get an HTTP response of 200. When axios does not get a successful response, it throws an error. I want to verify that console.log gets called twice in this case.
Here's a snippet of the class I'm testing:
class App extends React.Component {
...
async componentDidMount() {
let url = "/api/info/tmp"
try {
let response = await axios.get(url);
...do stuff
this.setState(...);
} catch (e) {
console.log("Could not get " + url);
console.log(e);
}
}
...
}
And here's a snippet of my jest test
let mockAxios = new MockAdapter(axios);
...
describe("App - componentDidMount() behavior test", () => {
beforeEach(() => {
app = shallow(<App />);
})
afterEach(() => {
app = undefined;
mockAxios.reset();
});
...
describe("Get " + url + " HTTP response status is not 200", () => {
beforeAll(() => {
mockAxios.onGet(url).reply(302, mockData);
});
it("Does not set state regardless of response body", () => {
console.log = jest.fn();
const state = app.state();
expect(console.log).toHaveBeenCalledTimes(2);
expect(state.solutions).toEqual({});
expect(state.username).toEqual("");
});
});
});
I know the console.log = jest.fn() bit is doing something because the console does not log the fake error anymore when I set it. However, the test fails because Expected mock function to have been called two times, but it was called zero times.
I've tried moving the console.log = jest.fn() into the "beforeEach", "beforeAll", and as a global variable.
UPDATE
I am pretty sure it's something to do with all the async that is going on.
If I do this:
it("Does not set state regardless of response body", async () => {
console.log = jest.fn();
await app.instance().componentDidMount();
expect(console.log).toHaveBeenCalledTimes(2);
const state = app.state();
expect(state.solutions).toEqual({});
expect(state.username).toEqual("");
});
Then the test still fails but my reason changed: Expected mock function to have been called two times, but it was called four times. Now I just got to figure out why it was called four times not twice.
UPDATE 2
I figured out why console.log was being called 4 times! Now I just need to figure out how I should refactor my tests.
If I comment out my jest mock, and even the whole unit test
it("Does not set state regardless of response body", async () => {
//const state = app.state();
//expect(state.solutions).toEqual({});
//expect(state.username).toEqual("");
//expect(console.log).toHaveBeenCalledTimes(2);
});
Then I can count in my console that there are already indeed two different console.log calls. shallow(<App />) must be already calling componentDidMount() or something. When I add app.instance().componentDidMount(), I can visually see that it is logging 4 times.
Updated Answer
Since it looks like you already know what you're doing with mocks, perhaps the issue has to do with componentDidMount().
I believe that your call to shallow(<App />) will already call App's componentDidMount() one time (which means your console.log will get called twice there).
Then, you subsequently call app.instance().componentDidMount() - that is, you call componentDidMount() again (which means your console.log will get called twice there again).
So, total... 4 calls to console.log.
Hope that points you in the right direction...
Original Answer
Actually, your question looks quite similar to [this StackOverFlow question on how to "How to mock console when it is used by a third-party library?"
You can use Jest mock functions to spyOn the global.console object.
For example, your test may look like this:
// Setup jest to spy on the console
const consoleSpy = jest.spyOn(global.console, 'log')
describe('App - componentDidMount() behavior test', () => {
beforeEach(() => {
jest.resetAllMocks() // reset your consoleSpy state back to initial
app = shallow(<App />)
})
...
it('Does not set state regardless of response body', () => {
const spy = jest.spyOn(global.console, 'log')
const state = app.state()
expect(consoleSpy).toHaveBeenCalledTimes(2)
expect(state.solutions).toEqual({})
expect(state.username).toEqual('')
})
...
Ideally, you'd move your API call outside of componentDidMount and into its own class method. Thay way it can be manually invoked from a lifecycle method or from an event callback. Also, you should anticipate the response to affect your UI state in some fashion (example: displaying a message to the user that the request failed and to try again).
The following example can be done with .then/.catch instead of async/await. Either way, you're working with Promises that are asynchronous and therefore they need asynchronous tests.
Note: The below assumes disableLifecycleMethods is true in the enzyme adapter. Also, just testing state changes (or a console.log) is a bit superfluous; instead, you would test if a component is rendered based upon the current state.
Working example: https://codesandbox.io/s/939w229l9r (includes both end to end and integration tests --- you can run the tests by clicking on the Tests tab located near the bottom left of the sandbox)
App.js (this will be a container that holds all relevant state and disperses it to its children as needed)
import React, { Component } from 'react';
class App extends Component {
state = = {
error: "",
isLoading: true,
solutions: {},
username: ""
};
componentDidMount() {
this.fetchData("/api/info/tmp");
}
fetchData = async (url) => {
try {
const res = await axios.get(url);
...do stuff
this.setState({
error: "",
isLoading: false,
solutions: res.data.solutions,
username: res.data.username
});
} catch (err) {
this.setState({
error: err,
isLoading: false,
solutions: {},
username: ""
});
}
}
render() { ... }
}
App.test.js (this assumes you'd want an end to end test)
import { shallow } from 'enzyme';
import App from './App';
const timeout = () =>
new Promise(resolve => {
setTimeout(() => {
resolve();
}, 2000);
});
const initialState = {
error: "",
isLoading: true,
solutions: {},
username: ""
};
describe("App", () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<App />);
wrapper.setState({ ...initialState });
});
afterAll(() => {
wrapper.unmount();
});
it("sets data to state based upon successful API call", async () => {
wrapper.instance().fetchData("/api/info/tmp");
await timeout();
wrapper.update();
expect(wrapper.state('isLoading')).toBeFalsy();
expect(wrapper.state('solutions')).toEqual({ somedata });
expect(wrapper.state('username')).toEqual("Some User");
});
it("displays an error upon unsuccessful API call", async () => {
wrapper.instance().fetchData("/api/bad/url");
await timeout();
wrapper.update();
expect(wrapper.state('isLoading')).toBeFalsy();
expect(wrapper.state('solutions')).toEqual({});
expect(wrapper.state('username')).toEqual("");
expect(wrapper.state('error')).toEqual("No data found.");
});
});
App.test.js (this assumes you'd want an integration test)
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import React from "react";
import { shallow } from "enzyme";
import App from "../App";
const solutions = [{ ... }, { ... }];
const username = "Some User"
const mockAxios = new MockAdapter(axios);
const initialState = {
error: "",
isLoading: true,
solutions: {},
username: ""
};
describe("App", () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<App />);
wrapper.setState({ ...initialState });
});
afterEach(() => {
mock.reset();
});
afterAll(() => {
mock.restore();
wrapper.unmount();
});
it("displays an error upon unsuccessful API call", async () => {
try {
mockAxios.onGet("/users").networkErrorOnce();
await axios.get("users");
} catch (err) {
const error = err.toString();
wrapper.setState({
error,
isLoading: false,
solutions: {},
username: ""
});
wrapper.update();
expect(wrapper.state('isLoading')).toBeEqual(error);
expect(wrapper.state('isLoading')).toBeFalsy();
expect(wrapper.state('solutions')).toEqual({});
expect(wrapper.state('username')).toEqual("");
}
});
it("sets data to state based upon successful API call", async () => {
try {
mockAxios.onGet("/users").reply(200, { solutions, username });
const res = await axios.get("users");
wrapper.setState({
error: "",
isLoading: true,
solutions: res.data.solutions,
username: res.data.username
});
wrapper.update();
expect(wrapper.state('isLoading')).toBeFalsy();
expect(wrapper.state('solutions')).toEqual(solutions);
expect(wrapper.state('username')).toEqual(username);
} catch (e) {
console.log(e);
}
});
});
I figured it out! Kind of... I am not certain why it works like this, but setting the mock in the actual "it" did not work.
The solution was making a beforeEach and afterEach
describe("Get " + url + " HTTP response status is not 200", () => {
beforeAll(() => {
mockAxios.onGet(url).reply(302, mockData);
});
beforeEach(() => {
console.log = jest.fn();
});
afterEach(() => {
jest.resetAllMocks();
});
it("Does not set state regardless of response body", async () => {
const state = app.state();
expect(state.solutions).toEqual({});
expect(state.username).toEqual("");
expect(console.log).toHaveBeenCalledTimes(2);
});
});
I'm writing unit tests for vuelidate validation in my component. I figured out that the $touch() method is called asynchronously, so I need to use $nextTick() for the expect(). The problem appears when I need two nextTick()s for two expect()s.
describe('Validations', () => {
let data
let myComponent
beforeEach(() => {
data = () => {
propertyABC = 'not allowed value'
}
myComponent = localVue.component('dummy', {template: '<div></div>', validations, data})
it('Properly validates propertyABC', (done) => {
Vue.config.errorHandler = done
let wrapper = mount(myComponent, {localVue})
wrapper.vm.$v.$touch()
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.$v.propertyABC.$error).to.be.true
# fails, because propertyABC === 'allowed value', adn thus $error is false
done()
}
wrapper.vm.propertyABC = 'allowed value'
wrapper.vm.$v.propertyABC.$touch()
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.$v.proprtyABC.$error).to.be.false
done()
}
})
})
How can I run this test without splitting it into two separate tests? I think nesting the $nextTick() might work, but it would be not flexible for higher amount of tests.
If you're able to use async functions, then you could await the $nextTick calls. This would avoid having to nest them and would keep everything in the same test.
Like so:
describe('Validations', () => {
let data;
let myComponent;
beforeEach(() => {
data = () => ({ propertyABC = 'not allowed value' });
myComponent = localVue.component('dummy', {template: '<div></div>', validations, data});
});
it('Properly validates propertyABC', async () => {
let wrapper = mount(myComponent, {localVue});
wrapper.vm.$v.$touch();
await wrapper.vm.$nextTick();
expect(wrapper.vm.$v.propertyABC.$error).to.be.true;
wrapper.vm.propertyABC = 'allowed value';
wrapper.vm.$v.propertyABC.$touch();
await wrapper.vm.$nextTick();
expect(wrapper.vm.$v.proprtyABC.$error).to.be.false;
})
})
Another approach is to use flushPromises.
import flushPromises from 'flush-promises';
...
test('some async test', async () => {
const wrapper = mount(MyComponent, { localVue });
wrapper.vm.$v.$touch();
await flushPromises();
});
flushPromises() returns a promise itself, so its useful when you need/want to chain things using .then().then() etc...