问题
I'm writing an app using React Native and Redux. I am designing a login form and want to test the components handle submit function. Within the handleSubmit()
functions several actions should be dispatched to Redux. Let me give you the handleSubmit()
functions code and the tests for it. First the function itself:
handleSubmit = (values, formikBag) => {
formikBag.setSubmitting(true);
const { loginSuccess, navigation, setHouses, setCitizens } = this.props;
apiLoginUser(values.email, values.password)
.then(data => {
const camelizedJson = camelizeKeys(data.user);
const normalizedData = Object.assign({}, normalize(camelizedJson, userSchema));
loginSuccess(normalizedData);
const tokenPromise = setToken(data.key);
const housePromise = getHouseList();
Promise.all([tokenPromise, housePromise])
.then(values => {
setHouses(values[1]);
getCitizenList(values[1].result[0])
.then(citizens => {
setCitizens(citizens);
formikBag.setSubmitting(false);
navigation.navigate("HomeScreen");
})
.catch(err => {
formikBag.setSubmitting(false);
alert(err);
});
})
.catch(err => {
console.log(err);
formikBag.setSubmitting(false);
alert(err);
});
})
.catch(error => {
alert(error);
formikBag.setSubmitting(false);
});
};
As you can see I'm also using normalizr to parse the data. The data of the getHouseList()
and getCitizenList()
functions are normalized within the respective functions.
Here are the tests:
const createTestProps = props => ({
navigation: { navigate: jest.fn() },
loginSuccess: jest.fn(),
setHouses: jest.fn(),
setCitizens: jest.fn(),
...props
});
...
describe("component methods", () => {
let wrapper;
let props;
beforeEach(() => {
props = createTestProps();
wrapper = shallow(<LoginForm {...props} />);
fetch.mockResponseOnce(JSON.stringify(userResponse));
fetch.mockResponseOnce(JSON.stringify(housesResponse));
fetch.mockResponseOnce(JSON.stringify(citizensResponse));
wrapper
.instance()
.handleSubmit({ email: "abc", password: "def" }, { setSubmitting: jest.fn() });
});
afterEach(() => {
jest.clearAllMocks();
});
it("should dispatch a loginSuccess() action", () => {
expect(props.loginSuccess).toHaveBeenCalledTimes(1);
});
});
In this test the values supplied to the jest-fetch-mocks
(userResponse
, housesResponse
and citizensResponse
) are constants. I now this test fails, because apparently the loginSuccess()
which should dispatch a Redux action is never called (even though I supplied a jest.fn()
in the createProps()
function).
What am I doing wrong? Why is the loginSuccess()
function never called?
EDIT: Upon request from Brian, here is the code for the api call:
export const apiLoginUser = (email, password) =>
postRequestWithoutHeader(ROUTE_LOGIN, { email: email, password: password });
export const postRequestWithoutHeader = (fullUrlRoute, body) =>
fetch(fullUrlRoute, {
method: "POST",
body: JSON.stringify(body),
headers: { "Content-Type": "application/json" }
}).then(response =>
response.json().then(json => {
if (!response.ok) {
return Promise.reject(json);
}
return json;
})
);
回答1:
ISSUE
The assertion on props.loginSuccess()
happens before the code that calls it has run.
DETAILS
It is important to remember that JavaScript is single-threaded and works off of a message queue (see Concurrency model and Event Loop).
It gets a message off of the queue, runs the associated function until the stack is empty, then returns to the queue to get the next message.
Asynchronous code in JavaScript works by adding messages to the queue.
In this case the calls to then()
within apiLoginUser()
are adding messages to the queue but between the end of beforeEach()
and it('should dispatch a loginSucces() action')
not all of them have had a chance to execute yet.
SOLUTION
The solution is to make sure the queued messages that eventually call loginSuccess()
all have a chance to run before performing the assertion.
There are two possible approaches:
Approach 1
Have handleSubmit()
return the promise created by apiLoginUser()
, then return that promise at the end of beforeEach()
. Returning a Promise
from beforeEach()
will cause Jest
to wait for it to resolve before running the test.
Approach 2
Waiting on the Promise
is ideal, but if the code cannot be changed then it is possible to manually delay the assertion in the test by the required number of event loop cycles. The cleanest way to do this is to make the test asynchronous and await a resolved Promise
(or series of promises if more than one event loop cycle is needed):
it('should dispatch a loginSuccess() action', async () => {
// queue the rest of the test so any pending messages in the queue can run first
await Promise.resolve();
expect(props.loginSuccess).toHaveBeenCalledTimes(1);
});
来源:https://stackoverflow.com/questions/51965312/react-native-enzyme-jest-mocked-redux-function-call-is-not-registered-is