开玩笑的单元测试以监视较低级别的方法(NodeJS)

Jest unit test to spy on lower-level method (NodeJS)

尝试使用 Jest 监视和重写函数的下一级。

测试结果说,"Expected mock function to have been called, but it was not called."

// mail/index.unit.test.js
import mail from './index';
import * as sib from '../sendinblue';

describe('EMAIL Util', () =>
  test('should call sibSubmit in server/utils/sendinblue/index.js', async() => {
    const sibMock = jest.spyOn(sib, 'sibSubmit');
    sibMock.mockImplementation(() => 'Calling sibSubmit()');
    const testMessage = {
      sender: [{ email: 'foo@example.com', name: 'Something' }],
      to: [{ email: 'foo@example.com', name: 'Something' }],
      subject: 'My Subject',
      htmlContent: 'This is test content'
    };
    await mail.send(testMessage);
    expect(sibMock).toHaveBeenCalled();
  })
);

mail.send() 来自这里...

// mail/index.js
import { sibSendTransactionalEmail } from '../sendinblue';

export default {
  send: async message => {
    try {
      return await sibSendTransactionalEmail(message);
    } catch(err) {
      console.error(err);
    }
  }
};

它通过 axios 使用 SendInBlue 的 API(为什么我需要模拟)...

// sendinblue/index.js
import axios from 'axios';
import config from '../../config/environment';

export async function sibSubmit(method, url, data) {
  let instance = axios.create({
    baseURL: 'https://api.sendinblue.com',
    headers: { 'api-key': config.mail.apiKey }
  });
  try {
    const response = await instance({
      method,
      url,
      data
    });
    return response;
  } catch(err) {
    console.error('Error communicating with SendInBlue', instance, err);
  }
}

export const sibSendTransactionalEmail = message => sibSubmit('POST', '/v3/smtp/email', message);

我假设 mail.send() 会在另一个模块中调用 sibSendTransactionalEmail(),它会调用 sibSubmit(),这是 jest.spyOn() 的焦点。想知道我哪里错了。

jest.spyOn replaces the method on the object it is passed with a spy.

在这种情况下,您传递的 sib 表示从 sendinblue.js 导出的 ES6 模块,因此 Jest 将替换 模块导出 对于 sibSubmit 与间谍并为间谍提供您提供的模拟实现。

mail.send 然后调用 sibSendTransactionalEmail 然后直接调用 sibSubmit.

换句话说,你的间谍没有被调用,因为 sibSendTransactionalEmail 没有为 sibSubmit 调用 模块导出 ,它只是调用 sibSubmit直接。

解决此问题的一个简单方法是注意 "ES6 modules support cyclic dependencies automatically",这样您就可以简单地将模块导入自身并使用模块导出从 sibSendTransactionalEmail 中调用 sibSubmit

import axios from 'axios';
import config from '../../config/environment';
import * as sib from './';  // import module into itself

export async function sibSubmit(method, url, data) {
  let instance = axios.create({
    baseURL: 'https://api.sendinblue.com',
    headers: { 'api-key': config.mail.apiKey }
  });
  try {
    const response = await instance({
      method,
      url,
      data
    });
    return response;
  } catch(err) {
    console.error('Error communicating with SendInBlue', instance, err);
  }
}

export const sibSendTransactionalEmail = message => sib.sibSubmit('POST', '/v3/smtp/email', message);  // call sibSubmit using the module export

注意用 jest.spyOn 替换 ES6 模块导出像这样

另一种解决此问题的方法是在模块中重新连接您正在监视的函数,这更好,因为您不必为了测试目的而修改原始代码。如果在 ES6 之前,您可以使用 rewire 模块,对于 ES6,您可以使用 babel-rewire

// mail/index.unit.test.js
import mail from './index';
import * as sib from '../sendinblue';

describe('EMAIL Util', () =>
  test('should call sibSubmit in server/utils/sendinblue/index.js', async() => {
    const sibMock = jest.spyOn(sib, 'sibSubmit');
    sibMock.mockImplementation(() => 'Calling sibSubmit()');
    //============ force the internal calls to use the mock also
    sib.__set__("sibSubmit", sibMock);  
    //============
    const testMessage = {
      sender: [{ email: 'foo@example.com', name: 'Something' }],
      to: [{ email: 'foo@example.com', name: 'Something' }],
      subject: 'My Subject',
      htmlContent: 'This is test content'
    };
    await mail.send(testMessage);
    expect(sibMock).toHaveBeenCalled();
  })
);