testing-library / react-hooks-testing-library

🐏 Simple and complete React hooks testing utilities that encourage good testing practices.
https://react-hooks-testing-library.com
MIT License
5.25k stars 231 forks source link

Testing hooks with useStripe() #476

Closed StefanoDeVuono closed 3 years ago

StefanoDeVuono commented 3 years ago

I'm testing a hook that calls methods on the stripe object returned by the useStripe() hook. As such, I'm trying to retrieve the stripe object from useStripe().

The problem is the context it's provided from gets a promise which later resolves. So useStripe starts by returning null and then returns a stripe object

Here's the provider:

// App.js
import { Elements } from '@stripe/react-stripe-js'
import { loadStripe } from '@stripe/stripe-js'
import React from 'react'
import CheckoutForm from './CheckoutForm'

// Make sure to call `loadStripe` outside of a component’s render to avoid
// recreating the `Stripe` object on every render.
const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLIC_KEY, {
  apiVersion: '2020-08-27',
})

const App = () => {
  return (
    <Elements stripe={stripePromise}>
      <CheckoutForm />
    </Elements>
  )
}

export default App

And here's my test

// App.test.js
import { useStripe } from '@stripe/react-stripe-js'
import { renderHook } from '@testing-library/react-hooks'
import React from 'react'
import App from './App'

test('useStripe hook', async () => {
  const wrapper = ({ children }) => <App>{children}</App>

  const { waitForNextUpdate, result } = renderHook(() => useStripe(), {
    wrapper,
  })

  await waitForNextUpdate()

  expect(result.current).not.toBeNull()
})

No dice. The test just times out. Any ideas?

mpeyper commented 3 years ago

If the test is timing out, that suggests that there are no state updates to trigger the hook to rerender. How many times does this log "renderer"?

  const { waitForNextUpdate, result } = renderHook(() => {
    console.log("rendered")
    return useStripe()
  }, {
    wrapper,
  })

You could also try using waitFor which is less prone to errors when the number of renders required to reach the desierd state changes:

  const { waitFor, result } = renderHook(() => useStripe(), {
    wrapper,
  })

  await waitFor(() => result.current !== null)

  expect(result.current).not.toBeNull() // or asserting the value you expect to be here instead
StefanoDeVuono commented 3 years ago

Thank you for your help! This is a great suggestion. Here are my findings

I think that's the crux of the problem. If I put console.log('render CheckoutForm') in my CheckoutForm.js, it shows it's rendering twice in the browser and only once in my test. Digging deeper into the Elements.tsx file of Stripe, I see that it takes the stripe promise and sets the context once the promise is resolved if stripe is available and isMounted.current is truthy:

// from Elements.tsx
parsed.stripePromise.then(function (stripe) {
        if (stripe && isMounted.current) {
          // Only update Elements context if the component is still mounted
          // and stripe is not null. We allow stripe to be null to make
          // handling SSR easier.
          setContext({
            stripe: stripe,
            elements: stripe.elements(options)
          });
        }
      });

When I debug the test, I see that the parsed.stripePromise object exists as a promise and , but the callback never executes. Is renderHook throwing away my Element component before it has a chance to resolve the promise within it?

mpeyper commented 3 years ago

No, renderHook doesn't throw anything away. I've just noticed that App does not render children at all, so const wrapper = ({ children }) => <App>{children}</App> is not going to work in this case. I think you want o be doing something like this:

  const wrapper = ({ children }) => <Elements stripe={mockedStripePromise}>{children}</Elements>

Unless your intention is actually not to be testing useStripe (which would make sense not to test a third party hook) but rather to test your App or CheckoutForm components. If that's the case then you are using the wrong library and should just be using @testing-library/react and rendering the component.

Natalia504 commented 2 years ago

@StefanoDeVuono , have you ever got to resolve this issue? I could really use some help here. Exact same issue here, i don't get stripe , seems like the Promise never resolves.

mpeyper commented 2 years ago

Hey @Natalia504, if you share some more details about your test and hook code I’m happy to take a look and see if anything stands out. Perhaps consider raising a new issue so we don’t keep notifying others with it though.