TheMcMurder
TheMcMurder

Reputation: 758

Testing React Async with Jest and create-react-app

I can't seem to figure this one out. I'm using create-react-app and it's built in test runner Jest. For all synchronous code it seems to work really well, but when mocking promises I can't seem to get it to work.

A react component has a form that I'm able to simulate a submit.

React component code snippets.

//Top of the page
import {auth} from '../../lib/API_V2'
// ... //

// Handle submit runs when the form is submitted
handleSubmit = (event) => {
  console.log('submit')
  event.preventDefault()
  this.setState(prevState => ({
    ...prevState,
    loading: true
  }))
  console.log('stateSet')
  auth(this.state.userName, this.state.password)
    .then(results => {
      // NEVER RUNS
      console.log('then')
      // stuff omitted
      this.setState(prevState => ({
        ...prevState,
        loading: false
      }))
      this.props.afterAuth()
    })
  .catch(() => {
    // also never runs
    // omitted
    this.setState(prevState => ({
      ...prevState,
      loading: false
    }))
    this.props.afterAuth()
  })
}

Test code

jest.mock('../../lib/API_V2')
it.only(`should mock a login`, () => {
  const myMock = jest.fn()
  const authComp = mount(<AuthComponent afterAuth={myMock}/>)

  authComp.find('.userName').simulate('change', {target: {value: 'userName'}})
  authComp.find('.password').simulate('change', {target: {value: 'password'}})
  expect(authComp.state().userName).toEqual('userName')
  expect(authComp.state().password).toEqual('password')
  authComp.find('[type="submit"]').get(0).click()
  expect(myMock.mock.calls.length).toBe(1) // FAILS
})

The API lib returns a promise. Instead of using that I have a __mocks__/API_V2.js next to it. That looks like this

function auth (lastname, accountNumber) {
  console.log('yay!?')
  return new Promise((resolve) => {
    resolve({
      accountNumber,
      lastName: lastname
    })
  })
}     

My mock test code never seems to be run. If I log the mock function I get function auth() {return mockConstructor.apply(this,arguments);}

I've tried to follow the instructions https://facebook.github.io/jest/docs/tutorial-async.html but it seems as though my mock methods aren't being called. And neither are the actual methods. Instead my call to auth() returns undefined.

Anyone have any ideas?

-- Supplementary Information --

src
  Components
    AuthComponent
      AuthComponent.js
      AuthComponent.test.js
      index.js
  Lib
    API_V2
      API_V2.js
      index.js
      __mocks__
        API_V2.js

Upvotes: 4

Views: 1730

Answers (2)

Suchipi
Suchipi

Reputation: 773

In the new Promise from your mock, even though you immediately resolve, this resolution does not occur synchronously. Promise callbacks always run as an enqueued microtask, so when you simulate a click in your test, the Promise callback in your mock has not yet run (and so myMock has not been called yet, either). This is why your expectation fails.

One (somewhat hacky) way you could work around this issue would be with a setTimeout. setTimeout will enqueue a task, and tasks always run after microtasks. Jest supports async tests via returning Promises from it callbacks, so you might write:

jest.mock('../../lib/API_V2')
it.only(`should mock a login`, () => new Promise(resolve => {
  const myMock = jest.fn()
  const authComp = mount(<AuthComponent afterAuth={myMock}/>)

  authComp.find('.userName').simulate('change', {target: {value: 'userName'}})
  authComp.find('.password').simulate('change', {target: {value: 'password'}})
  expect(authComp.state().userName).toEqual('userName')
  expect(authComp.state().password).toEqual('password')
  authComp.find('[type="submit"]').get(0).click()
  setTimeout(() => {
    expect(myMock.mock.calls.length).toBe(1)
    resolve() // Tell jest this test is done running
  }, 0);
}))

There is a good explanation of how tasks and microtasks work here: https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

Upvotes: 2

Paul Armstrong
Paul Armstrong

Reputation: 7156

I think you're hitting a bug related to this issue: https://github.com/facebook/jest/issues/2070

Since you're actually trying to import a file called API_V2/index.js, you need to mock index.js. However, you're going to have a really bad time doing that, since it'll be a valid mock for every index.js file that you try to mock.

The best way to do this at the moment is to rewrite some of your code to use dependency-injection and pass in a mock to whatever needs to use { auth }

Upvotes: 3

Related Questions