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


131

Я використовую mocha для тестування програми, написаної для 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не експортується.

Який правильний спосіб приватного тестування приватних методів? Чи є в моккі деякі вбудовані методи для цього?


Відповіді:


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

2
Я здогадуюсь, що якщо ви маєте повне покриття, ви протестуєте всі свої приватні функції, незалежно від того, ви їх відкрили чи ні.
Ziggy

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

3
@Jaro Більшість мого коду або у вигляді модулів AMD, з якими перемотка не в змозі впоратися (оскільки модулі AMD є функціями, але перемотування не може обробляти "змінні в межах функцій"). Або транслірується інший сценарій, з яким переможець не може впоратися. Власне, людям, які збираються переглядати перемикач, було б добре ознайомитись з обмеженнями (пов'язаними раніше), перш ніж спробувати їх використати. У мене немає жодного додатку, який а) потребує експорту "приватних" речей і б) не стикається з обмеженням перемоги.
Луї

1
Лише невеликий момент, покриття кодом може не підібрати тести, написані так. Принаймні, це я бачив, використовуючи вбудований інструмент покриття Jest.
Майк Стійд

Rewire теж не добре грає з інструментом автоматичного знущання, який працює у джей. Я все ще шукаю спосіб скористатись перевагами жаху та отримати доступ до деяких приватних послуг.
btburton42

Тому я спробував зробити цю роботу, але я використовую машинопис, який, мабуть, спричиняє цю проблему. В основному я отримую наступне повідомлення про помилку: Cannot find module '../../package' from 'node.js'. Хтось знайомий з цим?
clu

rewire добре працює .ts, typescriptя бігаю за допомогою ts-node @clu
muthukumar selvaraj

24

Ось справді хороший робочий процес для перевірки ваших приватних методів, пояснений Філіпом Уолтоном, інженером Google у своєму блозі.

Принцип

  • Запишіть свій код зазвичай
  • Прив’яжіть ваші приватні методи до об'єкта в окремому кодовому блоці, позначте його, _наприклад
  • Обведіть цей блок коду коментарями початку та кінця

Потім використовуйте завдання збірки або власну систему збирання (для прикладу grunt-strip-коду), щоб зняти цей блок для виробничих побудов.

Ваші тестові версії мають доступ до ваших приватних програм, а ваші виробничі версії не мають.

Знімок

Напишіть свій код таким чином:

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
}

7
Я робив це в тих випадках, коли весь модуль справді призначений для приватного використання, а не для загального споживання. Але для модулів загального призначення я вважаю за краще викривати те, що мені потрібно для тестування, лише коли тестується код. Це правда, що в кінцевому рахунку немає нічого, що не завадило б комусь потрапити на приватні речі, підробивши тестувальне середовище, але коли хтось робить налагодження у власному додатку, я вважаю за краще, щоб вони не бачили символів, які не повинні бути частина публічного API. Таким чином, немає негайної спокуси зловживати API для цілей, для яких він не призначений.
Луї

2
Ви можете також використовувати вкладений синтаксис {... приватна : {працівника: працівник}}
Джейсон

2
Якщо модуль - це все чисті функції, то я не бачу цього недоліків. Якщо ви зберігаєте і мутуєте стан, то остерігайтеся ...
Ziggy

5

Я зробив для цього пакет npm, який вам може бути корисним: вимагаю-з

В основному ви відкриваєте непублічні методи:

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;

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. Я побачу це для браузера, але вузол. Ні. Я не кажу, що це не робиться, просто питання і ця відповідь, яку ми коментуємо, нічого не говорять про модулі amd. Тож без того, хто заявляє, що вони використовують завантажувач amd, експорт вузлів не повинен працювати з amd. Хоча я хочу зазначити, що Commonjs може вийти з експорту es6. Я просто сподіваюся, що одного разу ми всі зможемо використовувати лише один метод експорту.
jemiloii

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 і перевірив, як можна зробити тести одиниць за допомогою модуля перемотування . Я можу підтвердити, що це рішення просто працює.

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

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

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

Наприклад, замість того, щоб приватні методи були в тому ж файлі, що і загальнодоступні, як цей ...

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

... Ви розділите його так:

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

src / річ / внутрішній / helper1.js

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

src / річ / внутрішній / helper2.js

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

Таким чином, ви можете легко протестувати helper1і helper2як є, не користуючись Rewire та іншими "магіями" (які, як я виявив, мають свої больові точки під час налагодження або коли ви намагаєтесь зробити свій рух до TypeScript, не кажучи вже про бідніші зрозумілість для нових колег). А знаходження в підпапці, що називається 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;
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.