React-stripe-elements: Add testContext

Created on 31 Jul 2017  Â·  25Comments  Â·  Source: stripe/react-stripe-elements

Hi, It would be great if you extended this project with a mock Stripe object so that it is possible to write tests using these components without loading the Stripe API.

enhancement needs more info

Most helpful comment

For now, I use this. It makes my tests work, but it's not the complete API. What would be great is a configurable stripe object that could be set so that we could mimmic the outside behaviours of Stripe - i.e. something like:

const Stripe = StripeMock.nextPaymentWillFail('no-funds')

<StripeElement stripe={Stripe} /> 

Here's what I use now (combined with sinon):

class Element {
  mount() {}
  update() {}
  destroy() {}
  on() {}
}

export default function mockStripe() {
  const elements = {
    create: () => {
      return new Element()
    }
  }
  const card = {}
  card.mount = sinon.spy()
  card.on = sinon.spy()
  card.change = sinon.spy()
  //elements.create = sinon.stub().returns(card)
  const stripe = {
    createToken: () => {
      console.log('create token')
    },
    elements: sinon.stub().returns(elements)
  }

  function Stripe(key) {
    stripe.key = key
    return stripe
  }
  return { elements, card, stripe, Stripe }
}

All 25 comments

Thanks for the suggestion @tarjei! This is currently not something we are working on, but we will take it into consideration. Can you share some more details on how you would like to test your app and how Stripe Elements get in your way?

I would also like to re-echo that this would be a very useful addition to the module! My team encountered issues running our tests against any component that has Stripe as their child component. We attempted to solve this by wrapping the root component that we're testing with to pass in the stripe object in 's context, but that requires us to load the Stripe API script in our testing framework, which seems a bit much. Having some type of testing hook that would pass in a dummy stripe object into a Stripe Element's context (or maybe StripeProvider test mode?) would be incredibly helpful!

@pattishin Hi Patti, could you let me know what approach you guys took to be able to make Stripe element testing possible?

@hoonchoi Unfortunately, we weren't able to sufficiently test the Stripe Elements. 😞 We put it on hold for now and just resorted to shallow testing our child components that required them.

@pattishin Ahhh.. I guess I have to do the same or maybe I'll try injecting the API somehow... Thanks though!

For now, I use this. It makes my tests work, but it's not the complete API. What would be great is a configurable stripe object that could be set so that we could mimmic the outside behaviours of Stripe - i.e. something like:

const Stripe = StripeMock.nextPaymentWillFail('no-funds')

<StripeElement stripe={Stripe} /> 

Here's what I use now (combined with sinon):

class Element {
  mount() {}
  update() {}
  destroy() {}
  on() {}
}

export default function mockStripe() {
  const elements = {
    create: () => {
      return new Element()
    }
  }
  const card = {}
  card.mount = sinon.spy()
  card.on = sinon.spy()
  card.change = sinon.spy()
  //elements.create = sinon.stub().returns(card)
  const stripe = {
    createToken: () => {
      console.log('create token')
    },
    elements: sinon.stub().returns(elements)
  }

  function Stripe(key) {
    stripe.key = key
    return stripe
  }
  return { elements, card, stripe, Stripe }
}

@tarjei oh nice - that's definitely helpful! 🎊 thank you!

I know I'm a bit late to this but I have mocked Stripe the same way that it is being done in the tests for this project. i.e. use original implementations of everything but mock the window.Stripe global variable.

I was able to get a solution hacked together from the suggestions here, would the maintainers be interested in a PR with some documentation on how mock/stub the global Stripe object? I think it would be a nice way to encourage more robust testing practices around billing flows.

Hey @bjudson 👋

That sounds like a great idea for a blog post! We generally reserve the README for officially supported documentation, so we make sure to thoroughly evaluate everything we add to it.

That being said, I'm sure people would still find the work you've done to be valuable, so we strongly encourage that you write about it! And if you do, I'd certainly be curious to read what you have to say.

Thanks for your interest in react-stripe-elements! We're always eager to see what and how people are building with our tools.

If I understand correctly, there are two different kinds of concerns in this thread. I think it would be beneficial to discuss them separately:

  1. One problem is that it's difficult to unit test components that happen to have Stripe-related components as children, if you want to avoid having to load and run Stripe.js in unit tests. I believe this issue can be resolved in a few ways:
  2. You can use a shallow render in your test case so that the Stripe bits don't actually need to get rendered.
  3. Or you can use our async and SSR strategy, which will allow you to gracefully render component trees without a Stripe.js instance, and the Stripe Elements will simply not render.

  4. A second problem is that you might want to actually render Stripe Elements for functional tests and trigger certain behavior, like entering an invalid card number and testing that your integration renders the appropriate error message to the user. In this case, you do need to actually load Stripe.js because you want to test things it does (displaying the element and knowing a card is invalid). The awkward part is actually getting data into the elements, since they cannot accept card numbers as props and their content is rendered in an iframe where it's tricky for you to send events. There are a couple possible solutions here:

  5. The ideal solution is that we would ship a functional testing API for Elements, which would let you ask us to perform actions like filling out the card number or expiry to valid or invalid values so you can test how you handle those cases. We have thought about this a bit, but there are some tricky API problems and we do not have a specific timeline for doing this.
  6. A possible fallback solution is that if you are writing tests in Selenium, or another tool that allows you to muck with the actual DOM inside of cross-origin iframes, you can poke at the Element iframe, find a selector that gets you to the actual input you want to fill in, and do it manually. We expect that DOM structure to remain relatively stable, but it is not a part of our stable public API, so we can't promise it won't change in the future.

I hope this helps anyone who finds this issue and wants help testing their application with react-stripe-elements. We are very aware that this can be difficult and hope to improve the situation over time.

Please comment and let us know if these two issues, and the proposed workarounds, don't encompass everything you're having trouble with.

I think it would be great to have some sort of Mocked version of the StripeProvider component which would make testing parent and children components more thorough

I have found that when I need to test parent components that contain the payment form (for error messages or any UI changes) the easiest way was to simply provide a null value to the Stripeprovider which allows the parent UI to render without the stripe components.

Using Jest + Enzyme

mount(
     <StripeProvider stripe={null}>
          <ParentComponent />
     </StripeProvider>,
);

This allows me to run tests on the parent component and mock out responses from the server and check for UI updates

@asolove-stripe Nice breakdown of the concerns! So I'm looking at the second scenario for e2e testing using Cypress, and I'm trying to fill in each of the CC form fields by drilling into the iframe. By referencing the event handlers in https://github.com/stripe/react-stripe-elements/blob/master/src/components/Element.js, I tried this:

function fillCardNumber(value: string) {
  // To enable drilling into the iframe, I'm running Chrome like this:
  // open -n -a Google\ Chrome --args --disable-web-security --user-data-dir=/some/dir
  const iframe = document.querySelector('[data-test-id="card-number"] iframe')
    .contentDocument.documentElement;

  // Try to fill in the input here
  const input = iframe.querySelector('[name="cardnumber"]');
  input.value = value;

  // This doesn't seem to do anything
  input.dispatchEvent(new Event('change'));

  // This wipes whatever that was set by `input.value = value` above
  input.dispatchEvent(new Event('blur'));
}

However, upon submitting the form, all the values in the input gets wiped out, same thing on blur. I didn't spot anything in this repo that could be doing this, so I'm guessing this might be something done opaquely by https://js.stripe.com/v3/?

What can I do to make this form submission work?

@thatmarvin: We do not deliberately do anything to clear these inputs and I know of some Stripe users who have tests roughly like this that work.

However, we are aware of problems that prevent some web testing tools from working correctly with controlled React inputs. I'm not familiar with Cyprus, but here's an example in Appium where you can appear to type into inputs during a test, but the React components never get updated due to (screaming noise). I have a suspicion that this may be related to why you are seeing values get wiped out. Do your Cyprus tests work correctly for non-Stripe controlled React inputs?

@asolove-stripe I was manually running commands in the web console, and turns out that React just didn't like the events I was dispatching. However, it worked when I let Cypress set the input values instead. It helped that you ruled out Stripe. Thanks!

@thatmarvin Glad to hear that!

I also think a mock version could be useful. This is how I currently mock Stripe in jest. It was loosely based off of @tarjei's example:

<StripeProvider
  stripe={{
    createSource: jest.fn(),
    elements: () => ({
      create: () => ({
        mount: jest.fn(),
        update: jest.fn(),
        destroy: jest.fn(),
        on: jest.fn()
      })
    }),
    createToken: jest.fn()
  }}>

@klaaspieter Could you elaborate on this example? Where are you inputing your api key? Is this a class you've created? Currently all I'm trying to do is get the very first test that comes with React to pass, the 'it renders without crashing' test. I would love to have a mock wrapper that is initialized and will allow my app to build.

@yasso1am you don't need to provide a token if stripe is set on the StripeProvider. It's what makes loading Stripe async and server side rendering work.

@klaaspieter: interesting solution to this problem, thanks for sharing!

Last version by @klaaspieter is not valid anymore "Please pass a valid Stripe object to StripeProvider." and not matching the typescript definition so you will both get a JS and TS error). It would be great that Stripe would provide at least something as simple to provide as the mock object you use in your test.

https://github.com/stripe/react-stripe-elements/blob/master/src/components/Provider.test.js#L12

They use this for their tests

    stripeMockResult = {
      elements: jest.fn(),
      createToken: jest.fn(),
      createSource: jest.fn(),
      createPaymentMethod: jest.fn(),
      handleCardPayment: jest.fn(),
    };

I keep getting Error: Please pass either 'apiKey' or 'stripe' to StripeProvider. If you're using 'stripe' but don't have a Stripe instance yet, pass 'null' explicitly. even though am using it the way they are testing in https://github.com/stripe/react-stripe-elements/blob/master/src/components/Provider.test.js#L12

import React from 'react';
import { render, cleanup } from '@testing-library/react';
import { Elements, StripeProvider } from 'react-stripe-elements';
import * as nextRouter from 'next/router';
import Buy from '../Buy';
import CheckoutContext from '../../utils/context';

describe('Buy component tests', () => {
  let elementMock;
  let elementsMock;
  let stripeMock;

  beforeEach(() => {
    elementMock = {
      mount: jest.fn(),
      destroy: jest.fn(),
      on: jest.fn(),
      update: jest.fn()
    };
    elementsMock = {
      create: jest.fn().mockReturnValue(elementMock)
    };
    stripeMock = {
      elements: jest.fn().mockReturnValue(elementsMock),
      createToken: jest.fn(),
      createSource: jest.fn(),
      createPaymentMethod: jest.fn(),
      handleCardPayment: jest.fn(),
      handleCardSetup: jest.fn()
    };

    window.Stripe = jest.fn().mockReturnValue(stripeMock);
  });

  afterEach(cleanup);

  it('should render the CheckoutForm', () => {
    const checkoutContextValues = {};

    const tree = render(
      <CheckoutContext.Provider value={checkoutContextValues}>
        <StripeProvider apiKey='pk_test_xxx'>
          <Elements>
            <Buy />
          </Elements>
        </StripeProvider>
      </CheckoutContext.Provider>
    );

    console.log('Tree', tree.debug());
  });
});

Component to test

return (
    <StripeProvider apiKey={apiKey}>
      <Elements>
        {checkout.shipping_preference === ShippingPreference.IN_STORE || allCartProductsDownloadable(cart) ? (
          <CreditCardForm />
        ) : (
          <CreditCardAndShippingForm />
        )}
      </Elements>
    </StripeProvider>
  );

Hey @theghostyced mocking window.Stripe works just fine—I was able to use Jest with the latest version of react-stripe-elements without issue. It's not clear what component your "Component to test" code corresponds with, but looks like it's creating its own StripeProvider and throwing an error because the apiKey prop wasn't passed in to your component.

You should only create one StripeProvider in your component tree. Rather than create StripeProvider in the Buy component, for example, you should have the Buy component assume it's mounted beneath a StripeProvider+Elements pair and create a CardElement, etc:

…
  it('should render the CheckoutForm', () => {
    const checkoutContextValues = {};

    const tree = render(
      <CheckoutContext.Provider value={checkoutContextValues}>
        <StripeProvider apiKey='pk_test_xxx'>
          <Elements>
            <Buy />
          </Elements>
        </StripeProvider>
      </CheckoutContext.Provider>
    );
});

// … elsewhere …
class Buy extends React.Component {
   function render() {
        return <>{checkout.shipping_preference === ShippingPreference.IN_STORE || allCartProductsDownloadable(cart) ? (
          <CreditCardForm />
        ) : (
          <CreditCardAndShippingForm />
        )}</>;
   }
}

Hey @fred-stripe Funny thing is I was about to try this, thanks for the response, will let you know how it goes :)

Closing this as it's a relatively old issue and this project has migrated to React Stripe.js. If you believe this is still important, please re-open it there.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

indiesquidge picture indiesquidge  Â·  5Comments

sonarforte picture sonarforte  Â·  4Comments

stephenhuh picture stephenhuh  Â·  4Comments

DennisdeBest picture DennisdeBest  Â·  4Comments

iMerica picture iMerica  Â·  5Comments