React-testing-library: Custom Hooks with useContext

Created on 6 Feb 2019  路  19Comments  路  Source: testing-library/react-testing-library

First, I'd like to thank you for getting react-testing-library updated so quickly after the release of hooks!

Describe the feature you'd like:

I'd like to add an option to testHook() for testing custom hooks that use useContext() and need to be wrapped in the Provider.

Below is a minimal (albeit silly) example of how useContext() could be used in a custom hook. I've used useContext() in a similar manner to this in more complicated custom hooks.

// examples/react-context-hook.js
import React from 'react'

const NameContext = React.createContext('Unknown')

function useGreeting() {
  const name = useContext(NameContext)
  return `Hello, ${name}!`
}

export {NameContext, useGreeting}

Suggested implementation:

[Option 1] Providing the Context

// examples/__tests__/react-context-hook.js
import {testHook, cleanup} from 'react-testing-library'

import {NameContext, useGreeting} from '../react-context-hook'

afterEach(cleanup)

test('provides the default value from context', () => {
  let name
  testHook(() => (name = useGreeting()), NameContext)
  expect(name).toBe('Hello, Unknown!')
})

test('provides the custom value from context', () => {
  let name
  testHook(() => (name = useGreeting()), NameContext, 'CustomName')
  expect(name).toBe('Hello, CustomName!')
})

[Option 2] Providing a Fixture

// examples/__tests__/react-context-hook.js
import {testHook, cleanup} from 'react-testing-library'

import {NameContext, useGreeting} from '../react-context-hook'

afterEach(cleanup)

test('provides the value from context', () => {
  const Fixture = ({ children }) => (
    <NameContext.Provider>
      {children}
    </NameContext.Provider>
  )
  let name
  testHook(() => (name = useGreeting()), Fixture)
  expect(name).toBe('Hello, Unknown!')
})

test('provides the custom value from context', () => {
  const Fixture = ({ children }) => (
    <NameContext.Provider value="CustomName">
      {children}
    </NameContext.Provider>
  )
  let name
  testHook(() => (name = useGreeting()), Fixture)
  expect(name).toBe('Hello, CustomName!')
})

[Option 3] Providing a Component and Props

// examples/__tests__/react-context-hook.js
import {testHook, cleanup} from 'react-testing-library'

import {NameContext, useGreeting} from '../react-context-hook'

afterEach(cleanup)

test('provides the default value from context', () => {
  let name
  testHook(() => (name = useGreeting()), NameContext.Provider)
  expect(name).toBe('Hello, Unknown!')
})

test('provides the custom value from context', () => {
  let name
  testHook(() => (name = useGreeting()), NameContext.Provider, { value: 'CustomName' })
  expect(name).toBe('Hello, CustomName!')
})

[Option 4] Some variation on the options above

I like the conciseness of 1 and 3, but 2 seems like it would probably be the most versatile. These were just my initial thoughts, so there's probably something that will work better than any of these.

Describe alternatives you've considered:

I'm doing something similar to this right now, but I could probably update my helper to work more like how testHook is implemented.

import {render, cleanup} from 'react-testing-library'

import {NameContext, useGreeting} from '../react-context-hook'

function renderWithContext(node, {value, ...options}) {
  return render(
    <NameContext.Provider value={value}>
      {node}
    </NameContext.Provider>,
    options
  )
}

afterEach(cleanup)

test('provides the custom value from context', () => {
  function Fixture() {
    const greeting = useGreeting()
    return `${greeting}`
  }
  const { queryByText } = renderWithContext(<Fixture />)
  expect(queryByText('Hello, Unknown!')).toBeTruthy()
})

test('provides the custom value from context', () => {
  function Fixture() {
    const greeting = useGreeting()
    return `${greeting}`
  }
  const { queryByText } = renderWithContext(<Fixture />, { value: 'CustomName' })
  expect(queryByText('Hello, CustomName!')).toBeTruthy()
})

Teachability, Documentation, Adoption, Migration Strategy:

Examples from above can be used. Adding the additional argument would be a minor release probably, so existing tests wouldn't need to be updated.

Most helpful comment

Idea:

  • Add wrapper to the options object for render
  • Pass all options for testHook through to underlying render

Custom testHook:

>

EDIT: see https://github.com/kentcdodds/react-testing-library/issues/283#issuecomment-462516149 that uses the correct API for testHook

// define
const customTestHook = (callback, {wrapperProps}) =>
  testHook(callback, {
    wrapper: props => <NameContext.Provider {...wrapperProps} {...props} />
  })

// use
const [a, b] = customTestHook(myCustomHook, {wrapperProps: {value: "ABC"}})

Custom render:

  • If the provider doesn't require the ability to change the value:
// define
const customRender = (component) =>
  render(component, {wrapper: SomeContext.Provider})

// use
const {rerender} = customRender(<Abc />)
  • If the wrapper needs to accept props:
// define
const customRender = (component, {wrapperProps}) =>
  render(component, {
    // "props" for the wrapper option is probably just `{ children }`
    wrapper: props => <NameContext.Provider {...wrapperProps} {...props} />
  })

// use
const {rerender} = customRender(<Abc />, {wrapperProps: {name: 'Foo'}})

All 19 comments

I'm in favor. What do you think @donavon?

Actually nevermind.

I honestly think that textHook is a bit of an edge-case utility and I don't want to make it overly complex.

I suggest just going with the alternative you've described. That's a lot simpler IMO...

Yeah the only one that seems general enough to maybe include would be the fixture idea in option 2 which is something that could also be added to render itself.

Adding a fixture to render would do the job. I really liked the simplicity of not having to deal with components when testing the custom hook. In the more complicated case that I have, I'm also using useState and useEffect in my hook, so it's not always just a case of getting text from the context.

I recommend you give this a quick watch: https://www.youtube.com/watch?v=0e6WCQYg5tU&index=22&list=PLV5CVI1eNcJgCrPH_e6d57KRUTiDZgs0u

Then I recommend you create a setup function that works like that.

Thanks for sharing that video. I ended up doing this for my helper which I'm pretty happy with鈥攊t's closer to the existing testHook API. I can open a PR with an example like this if you think it would help other folks figure this out.

import {NameContext} from '../react-context-hook'

function TestHook({ callback }) {
  callback()
  return null
}

export function testHookWithNameContext(callback, { value, ...options }) {
  render(
    <NameContext.Provider value={value}>
      <TestHook callback={callback} />
    </NameContext.Provider>,
    options,
  )
}

I think maybe this would be good in: https://github.com/kentcdodds/react-testing-library-examples

What do you think @alexkrolick?

I think maybe this would be good in: kentcdodds/react-testing-library-examples

Yes, with the addition of some tests showing how to use it. Probably wouldn't want to even export the testHookWith helper - keep it local to the test for the particular hook.

Note that this is pretty much the recommended approach for anything requiring context.

Would you be willing to open a pull request for that @ajlende? You could do it in the browser :)

I want to add my two cents here. I believe that Context will become the go-to solution for mocking stuff. Today, I've tweeted the following.

import React, { useContext } from 'react'

const context = React.createContext(() => new Date())

export function useNow() {
  return useContext(context)
}

export const FixedNowProvider: React.FC<{ dateTime: Date }> = ({
  dateTime,
  children,
}) => {
  return React.createElement(
    context.Provider,
    { value: () => dateTime },
    children,
  )
}

https://twitter.com/danielk_cz/status/1094908590836596737

Point is that using more of such providers will make tests rather bloated. I am not saying it should be a part of testHook, but it's something that will be repeatedly useful for sure.

Hopefully, it will get even easier with RFC for Context.write.

I'm somewhat I'm favor of adding a "wrapper [component]" option to render and testHook. The problem with wrapping render to provide your required helpers is you also need to reimplement rerender and that involves both "render"and "act".

@kentcdodds what do you think?

you also need to reimplement rerender and that involves both "render"and "act".

Why do you need to reimplement rerender? Wouldn't have to do that if you did this right? https://github.com/kentcdodds/react-testing-library/issues/283#issuecomment-461227966

Won't the rerender returned by the wrapped render not include the wrapping component?

Oh, nevermind, I see now.... Hmmm, yeah. That rerender is a beast and pretty annoying to have to re-implement.

Ok, I'm in favor. But for the record I really want to avoid making testHook any more complex than it is currently.

And by that I mean: It's fairly simple and I don't think it should become complex.

Idea:

  • Add wrapper to the options object for render
  • Pass all options for testHook through to underlying render

Custom testHook:

>

EDIT: see https://github.com/kentcdodds/react-testing-library/issues/283#issuecomment-462516149 that uses the correct API for testHook

// define
const customTestHook = (callback, {wrapperProps}) =>
  testHook(callback, {
    wrapper: props => <NameContext.Provider {...wrapperProps} {...props} />
  })

// use
const [a, b] = customTestHook(myCustomHook, {wrapperProps: {value: "ABC"}})

Custom render:

  • If the provider doesn't require the ability to change the value:
// define
const customRender = (component) =>
  render(component, {wrapper: SomeContext.Provider})

// use
const {rerender} = customRender(<Abc />)
  • If the wrapper needs to accept props:
// define
const customRender = (component, {wrapperProps}) =>
  render(component, {
    // "props" for the wrapper option is probably just `{ children }`
    wrapper: props => <NameContext.Provider {...wrapperProps} {...props} />
  })

// use
const {rerender} = customRender(<Abc />, {wrapperProps: {name: 'Foo'}})

I'm in favor :+1:

Definitely, like that idea as it would allow to supply more providers if needed or actually any other environment stuff that might be needed by a hook.

Btw, a slight correction of your example, getting a result doesn't work the way you have here. It was rather confusing for me at first too...

// define
const testHookWithNameContext = (callback, wrapperProps) => {
  let result
  testHook(() => {
    result = callback()
  }, {
    wrapper: props => <NameContext.Provider {...wrapperProps} {...props} />
  })
  return result
}

// use
const [a, b] = testHookWithNameContext(myCustomHook, {value: "ABC"})

Implementation note: Please update TypeScript declaration accordingly.

interface TestHookOptions {
  wrapper: React.FunctionalComponent
}

This can be also closed now, implemented in #296

Was this page helpful?
0 / 5 - 0 ratings

Related issues

NiGhTTraX picture NiGhTTraX  路  3Comments

addamove picture addamove  路  3Comments

kangweichan picture kangweichan  路  3Comments

jaredmeakin picture jaredmeakin  路  3Comments

chasen-bettinger picture chasen-bettinger  路  3Comments