First, I'd like to thank you for getting react-testing-library updated so quickly after the release of hooks!
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}
// 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!')
})
// 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!')
})
// 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!')
})
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.
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()
})
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.
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:
wrapper to the options object for rendertestHook through to underlying renderCustom 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:
// define
const customRender = (component) =>
render(component, {wrapper: SomeContext.Provider})
// use
const {rerender} = customRender(<Abc />)
// 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
Most helpful comment
Idea:
wrapperto the options object forrendertestHookthrough to underlying renderCustom
testHook:>
Custom
render: