PmmZenha
PmmZenha

Reputation: 185

Mock Stripe API with jest in Node typescript

I'm stuck at trying to mock the Stripe API in order to perform tests. I have little experience with mocking functions with jest but I have already done a deep search on how to mock Stripe's API but non seems to be working.

My file structure is the following:

src/payment-gateways/stripe.ts

import Stripe from 'stripe'
import { PAYMENT_STRIPE_API_SECRET_KEY } from '../config'

const stripe = new Stripe(PAYMENT_STRIPE_API_SECRET_KEY, {
  apiVersion: '2020-08-27',
})

export default stripe

Then on my stripe.test.ts I call my API endpoints in order that in the middle of the logic it creates a stripe customer.

What I have already tried was:

src/tests/__mocks__/stripe.ts

export class Stripe {}
const stripe = jest.fn(() => new Stripe())


export default stripe

src/...../stripe.test.ts

import { Stripe } from 'stripe'

Stripe.prototype.customers = {
    create: jest.fn(() => ({
              id: 1,
           })),
  } as unknown as Stripe.CustomersResource

//And also tried this without __mocks__

jest.mock('../../../../../../payment-gateways/stripe', () => {
  return jest.fn(() => ({
    customers: {
      create: jest.fn(() =>
        Promise.resolve({
          id: '1',
        })
      ),
    },
  }))
})

describe('stripe workflow', ()=> {
   it('creates a customer', async () => {
      await apollo.mutate...
   .
   .
   .
 })
})

But I keep getting the error [[GraphQLError: Cannot read property 'create' of undefined]] both methods.

I guess I'm missing something on the way jest works with mocks

Upvotes: 2

Views: 4102

Answers (2)

Misha Bruml
Misha Bruml

Reputation: 139

Mocking Stripe API in node has been a real pain so I hope this comment is useful to someone. I have improved on @MrDiggles answer. I'm mocking out paymentIntent.create method, but the theory is the same. Really I've just made the nested mock functions return implicitly and removed a few unnecessary lines, plus satisfied TS complier. The key part here is the create: (...args: any) => mockPaymentsIntentsCreate(...args) as unknown, which allows the mock to be accessed outside of the jest.mock scope (and can therefore have behaviour changed for different tests, as demonstrated), but still be hoisted and so is run before stripe is imported in the code under test. I'm actually not 100% sure how this even works; using create: mockPaymentsIntentsCreate instead would give you a ReferenceError: Cannot access 'mockPaymentsIntentsCreate' before initialization. Hopefully someone cleverer than me can answer that!

index.ts

import Stripe from 'stripe';

const stripe = new Stripe('sk_test_...', {
    apiVersion: '2020-08-27',
});

export const createPaymentIntent = async (parameters: Stripe.PaymentIntentCreateParams) => stripe.paymentIntents.create(parameters);

index.spec.ts

import { createPaymentIntent } from '../../src/stripe-client';

const mockPaymentsIntentsCreate = jest.fn();

jest.mock('stripe', () => jest.fn(() => ({
    paymentIntents: {
        create: (...args: any) => mockPaymentsIntentsCreate(...args) as unknown,
    },
})));

describe('stripe-client', () => {
    test('create payment intent happy path', async () => {
        const paymentIntentsCreateResponse = { id: '123' };
        mockPaymentsIntentsCreate.mockResolvedValueOnce(paymentIntentsCreateResponse);

        const result = await createPaymentIntent({
            amount: 100,
            currency: 'gbp',
        });

        expect(result).toBe(paymentIntentsCreateResponse);
    });

    test('create payment intent unhappy path', async () => {
        const paymentIntentsCreateResponse = 'oops';
        mockPaymentsIntentsCreate.mockRejectedValueOnce(paymentIntentsCreateResponse);

        await expect(createPaymentIntent({
            amount: 100,
            currency: 'gbp',
        })).rejects.toBe(paymentIntentsCreateResponse);
    });
});

Upvotes: 2

MrDiggles
MrDiggles

Reputation: 768

You can accomplish this by mocking out the implementation of Stripe:

const mockedCustomerCreate = jest.fn();

jest.mock('stripe', () => {
  const customers = jest.fn();
  // @ts-ignore
  customers.create = (...args) = mockedCustomerCreate(...args);

  return jest.fn().mockImplementation(() => ({
    customers
  }));
});

describe('foo', () => {
  test('bar', async () => {
    mockedCustomerCreate.mockResolvedValue({});
    
    // your test stuff

    expect(mockedCustomerCreate).toBeCalledTimes(1);
  });
})

Couple things to note about this method:

  • the // @ts-ignore is necessary to force the typing
  • the customers.create needs to be wrapped in another function otherwise jest will complain
  • you will need to do something similar for each function you use on the Stripe object

Upvotes: 3

Related Questions