网站搜索

node.js - 在 Node TypeScript 中使用 jest 模拟 Stripe API


我在尝试模拟 Stripe API 以进行测试时遇到了困难。我在使用 jest 模拟函数方面经验不多,但我已经深入搜索了如何模拟 Stripe 的 API,但没有一个方法有效。

我的文件结构如下:

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

然后在我的 stripe.test.ts 中,我按顺序调用我的 API 端点,以便在逻辑的中间创建一个 Stripe 客户。

我已经尝试过的是:

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...
   .
   .
   .
 })
})

但是我在这两种方法中都一直收到错误 [[GraphQLError: 无法读取未定义的属性 'create']]。

我想我对 Jest 与模拟对象的工作方式有些不理解

答案#1

你可以通过模拟 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);
  });
})

关于这种方法,有几点需要注意:

  • // @ts-ignore 是强制类型检查所必需的
  • customers.create 需要被包装在另一个函数中,否则 jest 会报错
  • 你需要对在 Stripe 对象上使用的每个函数做类似的操作

答案#2

在 Node 中模拟 Stripe API 真是一件麻烦事,所以我希望这个评论对某些人有用。我改进了 @MrDiggles 的答案。我正在模拟 paymentIntent.create 方法,但原理是一样的。实际上,我只是让嵌套的模拟函数隐式返回,并删除了一些不必要的行,同时满足 TS 编译器。关键部分是 create: (...args: any) => mockPaymentsIntentsCreate(...args) as unknown,,它允许在 jest 外部访问这个模拟。 mock 作用域(因此可以针对不同测试改变其行为,如示例所示),但仍然会被提升,因此在测试代码中导入 stripe 之前就会运行。我实际上并不完全确定这是如何工作的;使用 create: mockPaymentsIntentsCreate 反而会给你一个 ReferenceError: Cannot access 'mockPaymentsIntentsCreate' before initialization。希望有比我更聪明的人能回答这个问题!

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);
    });
});