-
Notifications
You must be signed in to change notification settings - Fork 233
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
Testing custom hook throwing error for incorrect arguments #20
Comments
Hi @dhruv-m-patel, The issue here is that the error is actually being thrown in the React render lifecycle, so it is never raised inside your test. You can see this by changing one of the tests to be: try {
renderHook(() => useError(undefined, 'invalid data'))
fail('should never get here')
} catch (e) {
expect(e.message).toBe('argA must be a function')
} Now your test fails with the error having an unexpected message ( Obviously, this is less than ideal when wanting to test error cases, but it does cause us a bit of an issue of how to actually surface the error in these cases. The I'm open to suggestions, but I propose that we extend the With this change your test would look something like: test('Not providing valid argA should fail', () => {
const { result } = renderHook(() => useCustomHook(undefined, 'invalid data'))
expect(result.error.message).toBe('argA must be a function')
}) How does this sound? |
I like the idea of enhancing the |
@mpeyper, to clarify I would imagine renderHook would return result with |
I'm about to jump on a flight, so I might take a look at this while in the air. Quick question, would you expect |
Since the hook first validates the types of arguments and throws errors, I would say |
So I made a quick spike of this on my flight and it raised more questions than it answered. Essentially, I'm unable to find a common approach between the synchronous and asynchronous that feels right. The synchronous test looks something like test('should capture error', () => {
const { result } = renderHook(() => useError())
expect(result.error).toEqual(Error('some error'))
}) But the asynchronous test looks like test('should capture async error', async () => {
const { waitForNextUpdate } = renderHook(() => useAsyncError())
try {
await waitForNextUpdate()
fail('expected error to be thrown')
} catch (e) {
expect(e).toEqual(Error('some error'))
}
}) which is the opposite of what I previously said about the use of So the obvious solution to this is to go back on what I said and make the first test also use test('should capture error', () => {
try {
renderHook(() => useError())
} catch (e) {
expect(e).toEqual(Error('some error'))
}
}) This seems ok on the surface, as both versions conform to the same pattern and work more or less as one would expect, but now consider the case where the first render is fine, but a rerender throws test('should capture error' on rerender, () => {
const { result, rerender } = renderHook(() => useError())
expect(result.current.someValue).toEqual('some value')
try {
rerender()
} catch (e) {
expect(e).toEqual(Error('some error'))
}
// is using result valid here?
// what about calling rerender again?
// should the component wrapper unmount at this point?
}) Ok, so what if we go the other way and remove the test('should capture async error', async () => {
const { result, waitForNextUpdate } = renderHook(() => useAsyncError())
await waitForNextUpdate() // the assumption here is the the promise resolves instead of rejects so that assertions can be made on `result`
expect(result.error).toEqual(Error('some error'))
}) Again, this seems to satify my common approach hang up, but there is another issue with it (which also applies to the synchronous version). These tests are fine when you are asserting and expected failure, but what happens when we are asserting an expected result, but an error was thrown instead? test('should capture error', () => {
const { result } = renderHook(() => useError())
expect(result.current.someValue).toEqual('someValue') // Uncaught TypeError: Cannot read property 'someValue' of undefined
}) This is an incredibly unhelpful error to see because After discussing this issue with a colleague, they suggested perhaps using a test('should capture error', () => {
const { result } = renderHook(() => useError())
expect(result.current.someValue).toEqual('someValue') // Error: some error
}) but then the test asserting an expected error start to become a bit funky test('should capture error', () => {
const { result } = renderHook(() => useError())
try {
result.current
} catch (e) {
expect(e).toEqual(Error('some error'))
}
}) So my final thought is to actually do both. Use a getter to throw the error if the Does any of that make sense? Any other thoughts, ideas or comments for anyone? Am I overthinking this? |
Given my previous ramblings, these are the current tests I have passing: import { useState, useEffect } from 'react'
import { renderHook } from 'src'
describe('error hook tests', () => {
function useError(throwError) {
if (throwError) {
throw new Error('expected')
}
return true
}
const somePromise = () => Promise.resolve()
function useAsyncError(throwError) {
const [value, setValue] = useState()
useEffect(() => {
somePromise().then(() => {
setValue(throwError)
})
}, [throwError])
return useError(value)
}
test('should raise error', () => {
const { result } = renderHook(() => useError(true))
expect(() => {
expect(result.current).not.toBe(undefined)
}).toThrow(Error('expected'))
})
test('should capture error', () => {
const { result } = renderHook(() => useError(true))
expect(result.error).toEqual(Error('expected'))
})
test('should not capture error', () => {
const { result } = renderHook(() => useError(false))
expect(result.current).not.toBe(undefined)
expect(result.error).toBe(undefined)
})
test('should reset error', () => {
const { result, rerender } = renderHook((throwError) => useError(throwError), {
initialProps: true
})
expect(result.error).not.toBe(undefined)
rerender(false)
expect(result.current).not.toBe(undefined)
expect(result.error).toBe(undefined)
})
test('should raise async error', async () => {
const { result, waitForNextUpdate } = renderHook(() => useAsyncError(true))
await waitForNextUpdate()
expect(() => {
expect(result.current).not.toBe(undefined)
}).toThrow(Error('expected'))
})
test('should capture async error', async () => {
const { result, waitForNextUpdate } = renderHook(() => useAsyncError(true))
await waitForNextUpdate()
expect(result.error).toEqual(Error('expected'))
})
test('should not capture async error', async () => {
const { result, waitForNextUpdate } = renderHook(() => useAsyncError(false))
await waitForNextUpdate()
expect(result.current).not.toBe(undefined)
expect(result.error).toBe(undefined)
})
test('should reset async error', async () => {
const { result, waitForNextUpdate, rerender } = renderHook(
(throwError) => useAsyncError(throwError),
{
initialProps: true
}
)
await waitForNextUpdate()
expect(result.error).not.toBe(undefined)
rerender(false)
await waitForNextUpdate()
expect(result.current).not.toBe(undefined)
expect(result.error).toBe(undefined)
})
}) |
It is great to see |
So what happened with that? Does it merge to the final release? |
react-hooks-testing-library
version: 0.3.7react-testing-library
version: N/A (I haven't installed this package in my repo for testing)react
version: 16.8node
version: 8.11.2npm
(oryarn
) version: 5.6.0Relevant code or config:
What you did:
I am trying to implement a custom hook adding validation for the arguments to match the expectations. undefined arguments should result in throwing error.
What happened:
The custom hook itself works as expected, however when testing above using jest passes tests but prints errors in console:
Tests:
Error it prints in console with tests passing:
Reproduction:
CodeSandBox link: https://codesandbox.io/s/183om7054
Problem description:
I believe it would be ideal not to print the jsdom errors since the errors are catched and error message is verified using jest.
The text was updated successfully, but these errors were encountered: