модульное тестирование приватных функций с помощью mocha и node.js

131

Я использую мокко для модульного тестирования приложения, написанного для node.js

Интересно, возможно ли модульное тестирование функций, которые не были экспортированы в модуль.

Пример:

У меня есть много таких функций, определенных в foobar.js

function private_foobar1(){
    ...
}

function private_foobar2(){
    ...
}

и несколько функций, экспортированных как общедоступные:

exports.public_foobar3 = function(){
    ...
}

Тестовый пример структурирован следующим образом:

describe("private_foobar1", function() {
    it("should do stuff", function(done) {
        var stuff = foobar.private_foobar1(filter);
        should(stuff).be.ok;
        should(stuff).....

Очевидно, это не работает, поскольку private_foobar1не экспортируется.

Как правильно проводить модульное тестирование частных методов? Есть ли у мокко какие-нибудь встроенные методы для этого?

Fstab
источник
Связанный: stackoverflow.com/questions/14874208
dskrvk

Ответы:

64

Если функция не экспортируется модулем, она не может быть вызвана тестовым кодом вне модуля. Это связано с тем, как работает JavaScript, и Mocha не может этого избежать.

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

Слово «окружающая среда» здесь используется в широком смысле. Это может означать проверку process.envили что-то еще, что может сообщить модулю «вы сейчас тестируете». Примеры, когда мне приходилось это делать, были в среде RequireJS, и я использовал module.configдля этой цели.

Луис
источник
2
Условный экспорт значений несовместим с модулями ES6. Я получаюSyntaxError: 'import' and 'export' may only appear at the top level
aij
1
@aij да из - за ES6 экспорт статических вы не можете использовать import, exportвнутри блока. В конце концов, вы сможете делать такие вещи в ES6 с помощью системного загрузчика. Один из способов обойти это сейчас - использовать module.exports = process.env.NODE_ENV === 'production' ? require('prod.js') : require('dev.js')и хранить различия кода es6 в этих соответствующих файлах.
cchamberlain 01
2
Я предполагаю, что если у вас есть полное покрытие, вы тестируете все свои частные функции, независимо от того, раскрыли вы их или нет.
Зигги
1
@aij Вы можете условно экспортировать ... см. этот ответ: stackoverflow.com/questions/39583958/…
RayLoveless
187

Проверить перепайку модуль . Он позволяет вам получать (и манипулировать) частные переменные и функции внутри модуля.

Итак, в вашем случае использование будет примерно таким:

var rewire = require('rewire'),
    foobar = rewire('./foobar'); // Bring your module in with rewire

describe("private_foobar1", function() {

    // Use the special '__get__' accessor to get your private function.
    var private_foobar1 = foobar.__get__('private_foobar1');

    it("should do stuff", function(done) {
        var stuff = private_foobar1(filter);
        should(stuff).be.ok;
        should(stuff).....
barwin
источник
3
@Jaro Большая часть моего кода представлена ​​либо в виде модулей AMD, которые перекомпоновка не может обрабатывать (поскольку модули AMD являются функциями, но перекомпоновка не может обрабатывать «переменные внутри функций»). Или переносится, другой сценарий, с которым перемонтировать не может. На самом деле, людям, которые собираются взглянуть на rewire, было бы хорошо сначала прочитать ограничения (указанные ранее), прежде чем они попытаются его использовать. У меня нет ни одного приложения, которое а) требует экспорта «личных» вещей и б) не сталкивается с ограничением перепрограммирования.
Луи
1
Небольшой момент, покрытие кода может не подобрать тесты, написанные таким образом. По крайней мере, это то, что я видел, используя встроенный инструмент покрытия Jest.
Майк Стед
Rewire также не очень хорошо работает с инструментом авто-насмешки jest. Я все еще ищу способ использовать преимущества jest и получить доступ к некоторым частным варам.
btburton42
Итак, я попытался выполнить эту работу, но использую машинописный текст, который, как я предполагаю, вызывает эту проблему. В основном я получаю следующее сообщение об ошибке: Cannot find module '../../package' from 'node.js'. Кто-нибудь знаком с этим?
clu
rewire работает нормально .ts, typescriptя бегаю с помощью ts-node @clu
muthukumar selvaraj
24

Вот действительно хороший рабочий процесс для тестирования ваших частных методов, который объяснил в своем блоге инженер Google Филип Уолтон.

Принцип

  • Напишите свой код как обычно
  • Привяжите свои частные методы к объекту в отдельном блоке кода, отметьте его, _например,
  • Окружите этот блок кода начальными и конечными комментариями

Затем используйте задачу сборки или свою собственную систему сборки (например, grunt-strip-code), чтобы удалить этот блок для производственных сборок.

Ваши тестовые сборки имеют доступ к вашему частному api, а ваши производственные сборки - нет.

отрывок

Напишите свой код так:

var myModule = (function() {

  function foo() {
    // private function `foo` inside closure
    return "foo"
  }

  var api = {
    bar: function() {
      // public function `bar` returned from closure
      return "bar"
    }
  }

  /* test-code */
  api._foo = foo
  /* end-test-code */

  return api
}())

И такие твои черновые задачи

grunt.registerTask("test", [
  "concat",
  "jshint",
  "jasmine"
])
grunt.registerTask("deploy", [
  "concat",
  "strip-code",
  "jshint",
  "uglify"
])

Глубже

В более поздней статье объясняется, почему «тестирование частных методов».

Реми Бешерас
источник
1
Также был найден плагин webkit, который, похоже, может поддерживать аналогичный рабочий процесс: webpack-strip-block
JRulle
21

Если вы предпочитаете, чтобы это было просто, просто экспортируйте и частные члены, но четко отделенные от общедоступного API некоторым соглашением, например, добавьте к ним префикс _или вложите их в один частный объект.

var privateWorker = function() {
    return 1
}

var doSomething = function() {
    return privateWorker()
}

module.exports = {
    doSomething: doSomething,
    _privateWorker: privateWorker
}
famousgarkin
источник
7
Я делал это в тех случаях, когда весь модуль действительно предназначен для частного использования, а не для общего пользования. Но для модулей общего назначения я предпочитаю предоставлять то, что мне нужно для тестирования, только во время тестирования кода. Верно, что в конечном итоге нет ничего, что могло бы помешать кому-то добраться до личных вещей, подделав тестовую среду, но когда кто-то выполняет отладку своего собственного приложения, я бы предпочел, чтобы они не видели символы, которые не должны быть часть публичного API. Таким образом, сразу же не возникает соблазна использовать API в целях, для которых он не предназначен.
Louis
2
вы также можете использовать вложенный синтаксис {... private : {worker: worker}}
Джейсон
2
Если модуль состоит только из чистых функций, то я не вижу в этом никаких недостатков. Если вы сохраняете и изменяете состояние, то будьте осторожны ...
Зигги
5

Для этой цели я сделал пакет npm, который может вам пригодиться: require-from

В основном вы предоставляете непубличные методы:

module.testExports = {
    private_foobar1: private_foobar1,
    private_foobar2: private_foobar2,
    ...
}

примечание: testExports может быть любое допустимое имя, за исключением exportsконечно.

И из другого модуля:

var requireFrom = require('require-from');
var private_foobar1 = requireFrom('testExports', './path-to-module').private_foobar1;
DEADB17
источник
1
Я не вижу практической пользы в этом методе. Это не делает «частные» символы более личными. (Любой может вызвать requireFromс правильными параметрами.) Также, если модуль с textExportsзагружен requireвызовом до requireFrom его загрузки, requireFromвернется undefined. (Я только что это протестировал.) Хотя часто можно контролировать порядок загрузки модулей, это не всегда практично. (Как свидетельствуют некоторые вопросы Mocha по SO.) Это решение также обычно не работает с модулями типа AMD. (Я загружаю модули AMD в Node ежедневно для тестирования.)
Луи
Он не должен работать с модулями AMD! Node.js использует common.js, и если вы измените его на использование AMD, вы сделаете это необычно.
jemiloii
@JemiloII Сотни разработчиков ежедневно используют Node.js для тестирования модулей AMD. В этом нет ничего «необычного». Максимум, что вы можете сказать, это то, что Node.js не поставляется с загрузчиком AMD, но это мало что говорит, поскольку Node предоставляет явные перехватчики для расширения своего загрузчика для загрузки любого формата, разработанного разработчиками.
Луи
Это вне нормы. Если вам нужно вручную включить загрузчик amd, это не норма для node.js. Я редко вижу AMD для кода node.js. Я посмотрю в браузере, но node. Нет. Я не говорю, что этого не делается, просто вопрос и ответ, который мы комментируем, ничего не говоря о модулях amd. Таким образом, если никто не заявляет, что они используют загрузчик amd, экспорт узлов не должен работать с amd. Хотя я хочу отметить, что commonjs может выйти из-под экспорта es6. Я просто надеюсь, что однажды мы все сможем использовать один метод экспорта.
jemiloii 05
4

Я добавил дополнительную функцию, которую назвал Internal (), и оттуда возвращаю все частные функции. Затем эта функция Internal () экспортируется. Пример:

function Internal () {
  return { Private_Function1, Private_Function2, Private_Function2}
}

// Exports --------------------------
module.exports = { PublicFunction1, PublicFunction2, Internal }

Вы можете вызвать внутренние функции следующим образом:

let test = require('.....')
test.Internal().Private_Function1()

Мне больше всего нравится это решение, потому что:

  • только одна функция всегда экспортируется Internal () . Эта функция Internal () всегда используется для тестирования частных функций.
  • Легко реализовать
  • Низкое влияние на производственный код (только одна дополнительная функция)
Перес Ламед ван Никерк
источник
2

Я последовал за ответом @barwin и проверил, как можно выполнять модульные тесты с помощью модуля rewire . Я могу подтвердить, что это решение просто работает.

Модуль должен состоять из двух частей - публичной и частной. Для публичных функций это можно сделать стандартным способом:

const { public_foobar3 } = require('./foobar');

Для частного использования:

const privateFoobar = require('rewire')('./foobar');
const private_foobar1 = privateFoobar .__get__('private_foobar1');
const private_foobar2 = privateFoobar .__get__('private_foobar2');

Чтобы узнать больше о предмете, я создал рабочий пример с полным тестированием модуля, тестирование включает частную и общедоступную области.

Для получения дополнительной информации я рекомендую вам проверить статью ( https://medium.com/@macsikora/how-to-test-private-functions-of-es6-module-fb8c1345b25f ), полностью описывающую тему, она включает примеры кода.

Мацей Сикора
источник
2

Я знаю, что это не обязательно тот ответ, который вы ищете, но я обнаружил, что в большинстве случаев, если частную функцию стоит протестировать, стоит разместить ее в отдельном файле.

Например, вместо того, чтобы иметь частные методы в том же файле, что и общедоступные, например ...

ЦСИ / вещь / PublicInterface.js


function helper1 (x) {
    return 2 * x;
}

function helper2 (x) {
    return 3 * x;
}

export function publicMethod1(x) {
    return helper1(x);
}

export function publicMethod2(x) {
    return helper1(x) + helper2(x);
}

... вы разделите его так:

ЦСИ / вещь / PublicInterface.js

import {helper1} from './internal/helper1.js';
import {helper2} from './internal/helper2.js';

export function publicMethod1(x) {
    return helper1(x);
}

export function publicMethod2(x) {
    return helper1(x) + helper2(x);
}

ЦСИ / вещь / внутренний / helper1.js

export function helper1 (x) {
    return 2 * x;
}

ЦСИ / вещь / внутренний / helper2.js

export function helper2 (x) {
    return 3 * x;
}

Таким образом, вы можете легко проверить helper1и helper2как есть, без использования Rewire и другой «магии» (который я нашел, есть свои болевые точки во время отладки, или когда вы пытаетесь сделать свой шаг к машинопись, не говоря уже о бедных понятность для новых коллег). И то, что они находятся в подпапке с названием internalили что-то в этом роде, поможет избежать их случайного использования в непредусмотренных местах.


PS: Еще одна распространенная проблема с «частными» методов является то , что если вы хотите проверить publicMethod1и publicMethod2и издеваться хелперов, опять же , вам обычно нужно что - то вроде Rewire , чтобы сделать это. Однако, если они находятся в отдельных файлах, вы можете использовать для этого Proxyquire , который, в отличие от Rewire, не требует каких-либо изменений в процессе сборки, легко читается и отлаживается и хорошо работает даже с TypeScript.

Даниэль Кис-Надь
источник
1

Чтобы сделать частные методы доступными для тестирования, я делаю следующее:

const _myPrivateMethod: () => {};

const methods = {
    myPublicMethod1: () => {},
    myPublicMethod2: () => {},
}

if (process.env.NODE_ENV === 'test') {
    methods._myPrivateMethod = _myPrivateMethod;
}

module.exports = methods;
MFB
источник