Шутный юнит-тест для шпионажа по методу более низкого уровня (NodeJS)

Попытка шпионить и переопределить функцию на два уровня вниз, используя Jest.

Результаты теста говорят: "Ожидается, что фиктивная функция была вызвана, но она не была вызвана".

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

Который использует API SendInBlue через axios (почему мне нужно издеваться)...

// 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(). Интересно, где я ошибся.

1 ответ

Решение

jest.spyOn заменяет метод объекта, который передается шпионом.

В этом случае вы проходите sib который представляет экспорт модуля ES6 из sendinblue.js, так Jest заменит модуль экспорта на sibSubmit со шпионом и дайте шпиону ложную реализацию, которую вы предоставили.

mail.send тогда звонит sibSendTransactionalEmail который затем вызывает sibSubmit напрямую

Другими словами, ваш шпион не называется, потому что sibSendTransactionalEmail не вызывает экспорт модуля для sibSubmit просто звонит sibSubmit непосредственно.

Простой способ решить эту проблему - заметить, что "модули ES6 автоматически поддерживают циклические зависимости", поэтому вы можете просто импортировать модуль в себя и вызвать sibSubmit изнутри sibSendTransactionalEmail используя модуль экспорта:

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

Обратите внимание, что замена экспорта модуля ES6 на jest.spyOn как это работает, потому что Jest переносит модули ES6 в Node модули таким образом, что позволяет их мутировать

Другой способ обойти эту проблему - перепрограммировать функцию, за которой вы следите, в модуле, что удобнее, поскольку вам не нужно изменять исходный код для целей тестирования. Вы можете использоватьrewire модуль, если до ES6, или babel-rewire для ES6:

// 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();
  })
);
Другие вопросы по тегам