React Native + Enzyme + Jest: Mocked redux function call is not registered / is called zero times

喜你入骨 提交于 2020-01-16 09:45:29

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!