Шутный юнит-тест для шпионажа по методу более низкого уровня (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();
})
);