nestjs-stripe icon indicating copy to clipboard operation
nestjs-stripe copied to clipboard

How to Mock Stripe Client for Unit Tests

Open markusheinemann opened this issue 3 years ago • 2 comments

I'm trying to unit-test a service which is using the Stripe class. The setup is similarly as described in the README

@Injectable()
export class MyService {
  constructor(@InjectStripe() private readonly stripe: Stripe) {}

  createCheckout() {
    this.stripe.checkout.sessions.create({/** **/});
  }
}

In the unit test I provide a mock as known from other services.

beforeEach(async () => {
  const module: TestingModule = await Test.createTestingModule({
    providers: [MyService, { provide: Stripe, useClass: StripeMock }],
  }).compile();
});

The mock looks like this:

class Sessions {
  create(): void {
    return null;
  }
}

class Checkout {
  sessions: Sessions;

  constructor() {
    this.sessions = new Sessions();
  }
}

export class StripeMock {
  checkout: Checkout;

  constructor() {
    this.checkout = new Checkout();
  }
}

The problem is that NestJs dose not recognize the mock completely. It throws the given error:

    Nest can't resolve dependencies of the CheckoutService (?). Please make sure that the argument StripeToken at index [0] is available in the RootTestModule context.

    Potential solutions:
    - If StripeToken is a provider, is it part of the current RootTestModule?
    - If StripeToken is exported from a separate @Module, is that module imported within RootTestModule?

Are there some recommendations how to mock the Stripe instance in tests? It could be useful to add a short section to the documentation about testing/mocking, because it seems not straight forward as known from other NestJs services. Maybe it make sense to provide sth. like a StripeTestingModule within this package? If someone explains the problem to me I can get my hands dirty adding a related "Unit Testing/Mock" section to the docs.

markusheinemann avatar Nov 04 '21 00:11 markusheinemann

I also came across the same issue and started digging into the source code to understand what StripeToken dependency was all about. It turns out it's an exported constant.

After some testing I came up with two possible solutions in order to generate a proper Stripe mock

  1. Mock the StripeToken dependency as an import in your test module (and export it)
const stripeMock = () => ({
  refunds: { create: jest.fn() },
  invoices: { list: jest.fn() },
});

beforeEach(async () => {
  const module: TestingModule = await Test.createTestingModule({
  imports: [
    {
      module: class FakeModule {},
      providers: [{ provide: 'StripeToken', useValue: {} }],
      exports: ['StripeToken'],
    },
  ],
    providers: [
      MyService, 
      { provide: Stripe, useFactory: stripeMock }
    ],
  }).compile();
  
  let stripe = module.get(Stripe);
});
  1. Mock StripeToken instead of Stripe (less verbose, same as above)
const stripeMock = () => ({
  refunds: { create: jest.fn() },
  invoices: { list: jest.fn() },
});

beforeEach(async () => {
  const module: TestingModule = await Test.createTestingModule({
    providers: [
      MyService, 
      { provide: 'StripeToken', useFactory: stripeMock },
    ]
  }).compile();
  
  let stripe = module.get('StripeToken');
});

fer8a avatar Feb 08 '22 21:02 fer8a

Here's the way I got it working:

  1. Create a mock of the node stripelibrary
// __mocks__/stripe.mock.ts

import Stripe from 'stripe';
import * as faker from 'faker';

export const mockStripe = () => {
  return {
    customers: {
      retrieve: jest.fn((customerId: string) =>
        Promise.resolve({
          id: customerId,
        } as Stripe.Response<Stripe.Customer | Stripe.DeletedCustomer>),
      ),
      update: jest.fn(
        (customerId: string, params?: Stripe.CustomerUpdateParams) =>
          Promise.resolve({
            id: customerId,
            currency: 'usd',
            ...params,
          }),
      ),
      create: jest.fn((params?: Stripe.CustomerCreateParams) =>
        Promise.resolve({
          id: `cust_${faker.lorem.word(10)}`,
          ...params,
        }),
      ),
      del: jest.fn(() => Promise.resolve()),
    },
    subscriptions: {
      list: jest.fn(() =>
        Promise.resolve({
          data: [],
        }),
      ),
      retrieve: jest.fn((subscriptionId: string) =>
        Promise.resolve({
          id: subscriptionId,
          object: 'subscription',
        } as Stripe.Response<Stripe.Subscription>),
      ),
      create: jest.fn((params: Stripe.SubscriptionCreateParams) =>
        Promise.resolve({
          id: `sub_${faker.lorem.word(10)}`,
          object: 'subscription',
          ...params,
        }),
      ),
      update: jest.fn(
        (subscriptionId: string, params?: Stripe.SubscriptionUpdateParams) =>
          Promise.resolve({
            id: subscriptionId,
            object: 'subscription',
            ...params,
          }),
      ),
      del: jest.fn(() => Promise.resolve()),
    },
    invoices: {
      list: jest.fn(() =>
        Promise.resolve([
          {
            id: `invoice_${faker.lorem.word(10)}`,
          },
          {
            id: `invoice_${faker.lorem.word(10)}`,
          },
        ] as Stripe.Response<Stripe.Invoice>[]),
      ),
      retrieve: jest.fn((subscriptionId: string) =>
        Promise.resolve({
          id: subscriptionId,
          object: 'subscription',
        } as Stripe.Response<Stripe.Subscription>),
      ),
      retrieveUpcoming: jest.fn(
        (params?: Stripe.InvoiceRetrieveUpcomingParams) =>
          Promise.resolve({
            ...params,
          } as Stripe.Response<Stripe.Invoice>),
      ),
      create: jest.fn((params?: Stripe.InvoiceCreateParams) =>
        Promise.resolve({
          id: `invoice_${faker.lorem.word(10)}`,
          ...params,
        }),
      ),
      pay: jest.fn(() => Promise.resolve()),
    },
    testHelpers: {
      testClocks: {
        create: jest.fn((params: Stripe.TestHelpers.TestClockCreateParams) =>
          Promise.resolve({
            id: `test_clock_${faker.lorem.word(10)}`,
            ...params,
          }),
        ),
      },
    },
  };
};
  1. Create a mock of the nestjs-stripe module
// __mocks__/nestjs-stripe.mock.ts

import { DynamicModule, Inject, Module } from '@nestjs/common';
import { mockStripe } from './stripe.mock';

@Module({})
class MockStripeModule {
  public static forRootAsync(): DynamicModule {
    return {
      module: MockStripeModule,
      providers: [{ provide: 'StripeToken', useFactory: mockStripe }],
      exports: [{ provide: 'StripeToken', useFactory: mockStripe }],
    };
  }
}

export const mockNestJsStripe = () => ({
  InjectStripe: () => Inject('StripeToken'),
  StripeModule: MockStripeModule,
});
  1. Define this mock at the start of your .spec.ts files
// some-test.spec.ts
import { mockNestJsStripe } from '@mocks/nestjs-stripe.mock';
jest.mock('nestjs-stripe', () => mockNestJsStripe());

agrawal-rohit avatar Jul 23 '22 12:07 agrawal-rohit