Skip to content

Can't access the component's instance #58

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Gpx opened this issue Apr 16, 2018 · 5 comments
Closed

Can't access the component's instance #58

Gpx opened this issue Apr 16, 2018 · 5 comments

Comments

@Gpx
Copy link
Member

Gpx commented Apr 16, 2018

  • react-testing-library version: 2.1.1
  • node version: 9.4.0
  • npm (or yarn) version: 5.6.0

Relevant code or config

I'm trying to test this component

import React from 'react'
import pick from 'lodash.pick'
import { fetchTrips } from '../../../../../api/trips'

class TripListState extends React.Component {
  static defaultProps = { fetchTrips }

  state = { status: 'LOADING', filter: 'ALL' }

  async componentDidMount() {
    const trips = await this.props.fetchTrips('ALL')
    this.setState({ trips, status: 'READY' })
  }

  handleChangeFilter = async filter => {
    this.setState({ filter, status: 'FILTERING' })
    const trips = await this.props.fetchTrips(filter)
    this.setState({ trips, status: 'READY' })
  }

  render() {
    return this.props.children({
      ...pick(this.state, ['status', 'trips', 'filter']),
      onChangeFilter: this.handleChangeFilter,
    })
  }
}

export default TripListState

It simpliy fetches a list of trips and and exposes a method to change the filter value.

What you did:

This is my attempt to test it:

import React from 'react'
import { render, wait } from 'react-testing-library'
import TripListState from './TripListState'

describe('<TripListState>', () => {
  it('should fetch the trips on load', async () => {
    const children = jest.fn(() => null)
    const trips = [{ id: 1 }]
    // I pass this as a prop so I can avoid mocking the dependency
    const fetchTrips = x => Promise.resolve(trips)
    render(
      <TripListState children={children} fetchTrips={fetchTrips} />
    )
    // This works just fine
    expect(children).toHaveBeenCalledTimes(1)
    // This errors out
    expect(children).toHaveBeenCalledWith({
      status: 'LOADING',
      filter: 'ALL',
      onChangeFilter: /* Not sure what to put here */
    })
  })
})

What happened:

fail-test

Problem description:

The problem is that I can't access my component's instance and its methods. Not sure what the best approach is here.

Suggested solution:

I'm unsure of this, maybe I'm using react-testing-libraray for something it was not intended to, or maybe my testing approach is wrong. I'm hoping for suggestions :)

@gnapse
Copy link
Member

gnapse commented Apr 16, 2018

react-testing-library intentionally does not give you access to the component instance. It is intended to test the app as a user would experience it. However in this case I understand what you're trying to do.

I'd suggest that instead of passing a jest spy function as the render prop, you pass an actual render functions as children, one that renders something simple but that you can then test in the resulting rendered DOM, and that would allow you to check that status was "LOADING", filter was "ALL", and to test the side effects of invoking the event that would end up invoking onChangeFilter.

For instance, something like this:

import React from 'react'
import { render, wait } from 'react-testing-library'
import TripListState from './TripListState'

describe('<TripListState>', () => {
  it('should fetch the trips on load', async () => {
    const children = ({ status, filter }) => (
      <div>
        <div data-testid="status">{status}</div>
        <div data-testid="filter">{filter}</div>
      </div>
    );
    const trips = [{ id: 1 }]
    // I pass this as a prop so I can avoid mocking the dependency
    const fetchTrips = x => Promise.resolve(trips)
    render(
      <TripListState children={children} fetchTrips={fetchTrips} />
    )
    expect(getByTestId('status')).toHaveTextContent('LOADING');
    expect(getByTestId('filter')).toHaveTextContent('ALL');
  })
})

Checking of the onChangeFilter callback is not present in that suggested change, but I'm sure you get the gist and can complete it yourwelf.

@kentcdodds
Copy link
Member

Thanks @gnapse! For components that expose a render prop API, you may also consider that the "user of your software" is actually a developer who's using the render prop API to build another component, so in that case I'm actually (generally) fine with passing a spy as the render prop value. If you look at the tests in downshift, you'll see we do this a lot (for example). I hope these examples are a help to you. If you come up with something you're happy with, we'd really appreciate it if you could add an example to the __tests__ directory of this project and link to it in the README (similar to redux and react-router) :)

As this issue isn't exactly actionable, I'll go ahead and close it. Feel free to continue chatting though :)

@Gpx
Copy link
Member Author

Gpx commented Apr 17, 2018

Thanks to both for the help. This is what I ended up doing:

import React from 'react'
import { render, wait } from 'react-testing-library'
import TripListState from './TripListState'

describe('<TripListState>', () => {
  it('should fetch the trips on load', async () => {
    const children = jest.fn(() => null)
    const trips = [{ id: 1 }]
    const fetchTrips = () => Promise.resolve(trips)
    render(<TripListState children={children} fetchTrips={fetchTrips} />)
    expect(children).toHaveBeenCalledTimes(1)
    expect(children.mock.calls[0][0]).toMatchObject({
      status: 'LOADING',
      filter: 'ALL',
    })
    await wait(() => expect(children).toHaveBeenCalledTimes(2))
    expect(children.mock.calls[1][0]).toMatchObject({
      status: 'READY',
      filter: 'ALL',
      trips,
    })
  })

  it('should allow to change the filter', async () => {
    const children = jest.fn(() => null)
    const allTrips = [{ id: 1 }, { id: 2 }]
    const pastTrips = [allTrips[0]]
    const fetchTrips = filter =>
      Promise.resolve(filter === 'ALL' ? allTrips : pastTrips)
    render(<TripListState children={children} fetchTrips={fetchTrips} />)
    await wait(() => expect(children).toHaveBeenCalledTimes(2))
    children.mock.calls[1][0].onChangeFilter('PAST')
    expect(children).toHaveBeenCalledTimes(3)
    expect(children.mock.calls[2][0]).toMatchObject({
      status: 'FILTERING',
      filter: 'PAST',
      trips: undefined,
    })
    await wait(() => expect(children).toHaveBeenCalledTimes(4))
    expect(children.mock.calls[3][0]).toMatchObject({
      status: 'READY',
      filter: 'PAST',
      trips: pastTrips,
    })
  })
})

I don't care what onChangeFilter is as long as the side effect I get when I call it is the one I want.

@kentcdodds do you think it is worth to generalize this into __tests__?

@kentcdodds
Copy link
Member

That looks good! It does look a little specific to your use case to include in __tests__. So let's skip that for now. Thanks!

@MuYunyun
Copy link

MuYunyun commented Mar 1, 2021

@Gpx hi, if you have to test component's instance, you can try

  it('test instance exist', () => {
    let instance
    render(
      <Component
        ref={node => {
          instance = node
        }}
      />
    )
    expect(Object.prototype.toString.call(instance.A)).toBe('[object Function]')
    expect(Object.prototype.toString.call(instance.B)).toBe('[object Function]')
  })

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants