Skip to content
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

Custom Hooks with useContext #283

Closed
ajlende opened this issue Feb 6, 2019 · 19 comments
Closed

Custom Hooks with useContext #283

ajlende opened this issue Feb 6, 2019 · 19 comments

Comments

@ajlende
Copy link

ajlende commented Feb 6, 2019

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.

@kentcdodds
Copy link
Member

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

@kentcdodds
Copy link
Member

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...

@alexkrolick
Copy link
Collaborator

alexkrolick commented Feb 6, 2019

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.

@ajlende
Copy link
Author

ajlende commented Feb 6, 2019

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.

@kentcdodds
Copy link
Member

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.

@ajlende
Copy link
Author

ajlende commented Feb 6, 2019

Thanks for sharing that video. I ended up doing this for my helper which I'm pretty happy with—it'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,
  )
}

@kentcdodds
Copy link
Member

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

What do you think @alexkrolick?

@alexkrolick
Copy link
Collaborator

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.

@kentcdodds
Copy link
Member

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

kentcdodds pushed a commit to kentcdodds/react-testing-library-examples that referenced this issue Feb 7, 2019
This adds an example for how to test custom hooks that use `useContext()`.

React and react-testing-library were updated to [The One With Hooks](https://reactjs.org/blog/2019/02/06/react-v16.8.0.html).

Addresses testing-library/react-testing-library#283
@danielkcz
Copy link
Contributor

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.

@alexkrolick
Copy link
Collaborator

alexkrolick commented Feb 11, 2019

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?

@kentcdodds
Copy link
Member

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? #283 (comment)

@alexkrolick
Copy link
Collaborator

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

@kentcdodds
Copy link
Member

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.

@kentcdodds
Copy link
Member

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

@alexkrolick
Copy link
Collaborator

alexkrolick commented Feb 11, 2019

Idea:

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

Custom testHook:

EDIT: see #283 (comment) 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'}})

@kentcdodds
Copy link
Member

I'm in favor 👍

@danielkcz
Copy link
Contributor

danielkcz commented Feb 11, 2019

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
}

@danielkcz
Copy link
Contributor

This can be also closed now, implemented in #296

lucbpz pushed a commit to lucbpz/react-testing-library that referenced this issue Jul 26, 2020
…rary#283)

Co-authored-by: Greg Shtilman <gshtilma@yahoo-inc.com>
markmabery added a commit to markmabery/react-testing-library-examples that referenced this issue Mar 31, 2022
This adds an example for how to test custom hooks that use `useContext()`.

React and react-testing-library were updated to [The One With Hooks](https://reactjs.org/blog/2019/02/06/react-v16.8.0.html).

Addresses testing-library/react-testing-library#283
ben-kennedy1 added a commit to ben-kennedy1/react-testing-library-examples that referenced this issue Aug 8, 2022
This adds an example for how to test custom hooks that use `useContext()`.

React and react-testing-library were updated to [The One With Hooks](https://reactjs.org/blog/2019/02/06/react-v16.8.0.html).

Addresses testing-library/react-testing-library#283
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