วิธีการเข้าถึงและทดสอบฟังก์ชั่นภายใน (ไม่ใช่การส่งออก) ในโมดูล node.js?


181

ฉันพยายามหาวิธีการทดสอบฟังก์ชั่นภายใน (เช่นไม่ส่งออก) ใน nodejs (ควรมีมอคค่าหรือจัสมิน) และฉันก็ไม่รู้เลย!

สมมติว่าฉันมีโมดูลเช่นนั้น:

function exported(i) {
   return notExported(i) + 1;
}

function notExported(i) {
   return i*2;
}

exports.exported = exported;

และการทดสอบต่อไปนี้ (มอคค่า):

var assert = require('assert'),
    test = require('../modules/core/test');

describe('test', function(){

  describe('#exported(i)', function(){
    it('should return (i*2)+1 for any given i', function(){
      assert.equal(3, test.exported(1));
      assert.equal(5, test.exported(2));
    });
  });
});

มีวิธีทดสอบหน่วยnotExportedฟังก์ชั่นโดยไม่ส่งออกจริง ๆ เพราะไม่ได้มีการเปิดเผยหรือไม่?


1
บางทีแค่ฟังก์ชั่นการทดสอบเมื่ออยู่ในสภาพแวดล้อมที่เฉพาะเจาะจง? ฉันไม่รู้ขั้นตอนมาตรฐานที่นี่
loganfsmyth

คำตอบ:


243

ReWireโมดูลแน่นอนคำตอบ

นี่คือรหัสของฉันสำหรับการเข้าถึงฟังก์ชั่นที่ไม่ได้ส่งออกและทดสอบโดยใช้ Mocha

application.js:

function logMongoError(){
  console.error('MongoDB Connection Error. Please make sure that MongoDB is running.');
}

test.js:

var rewire = require('rewire');
var chai = require('chai');
var should = chai.should();


var app = rewire('../application/application.js');


logError = app.__get__('logMongoError'); 

describe('Application module', function() {

  it('should output the correct error', function(done) {
      logError().should.equal('MongoDB Connection Error. Please make sure that MongoDB is running.');
      done();
  });
});

2
นี่ควรเป็นคำตอบที่ดีที่สุด ไม่จำเป็นต้องเขียนโมดูลที่มีอยู่ทั้งหมดด้วยการเอ็กซ์ปอร์ตเฉพาะ NODE_ENV และไม่เกี่ยวข้องกับการอ่านในโมดูลเป็นข้อความ
Adam Yost

ทางออกที่สวยงาม มันเป็นไปได้ที่จะไปต่อและรวมกับสายลับในกรอบการทดสอบของคุณ ทำงานกับจัสมินฉันลองใช้กลยุทธ์นี้
Franco

2
ทางออกที่ดี มีรุ่นใช้งานสำหรับคนประเภทบาเบลหรือไม่?
Charles Merriam

2
ใช้ rewire กับ jest และ ts-jest (typescript) ฉันได้รับข้อผิดพลาดดังต่อไปนี้: Cannot find module '../../package' from 'node.js'. คุณเคยเห็นสิ่งนี้ไหม
clu

2
Rewire มีปัญหาความเข้ากันได้กับตัวตลก Jest จะไม่พิจารณาฟังก์ชั่นที่เรียกจาก rewire ในรายงานความครอบคลุม ที่ค่อนข้างจะเอาชนะวัตถุประสงค์
robross0606

10

เคล็ดลับคือการตั้งค่าNODE_ENVตัวแปรสภาพแวดล้อมให้เป็นอย่างที่ต้องการtestแล้วส่งออกตามเงื่อนไข

สมมติว่าคุณไม่ได้ติดตั้งมอคค่าทั่วโลกคุณสามารถมี Makefile ในรูทของไดเรกทอรีแอปของคุณที่มีสิ่งต่อไปนี้:

REPORTER = dot

test:
    @NODE_ENV=test ./node_modules/.bin/mocha \
        --recursive --reporter $(REPORTER) --ui bbd

.PHONY: test

ไฟล์ make นี้ตั้งค่า NODE_ENV ก่อนรัน mocha จากนั้นคุณสามารถเรียกใช้การทดสอบมอคค่าด้วยmake testที่บรรทัดคำสั่ง

ตอนนี้คุณสามารถส่งออกฟังก์ชันของคุณแบบมีเงื่อนไขซึ่งโดยปกติจะไม่ส่งออกเฉพาะเมื่อการทดสอบมอคค่าของคุณทำงานอยู่:

function exported(i) {
   return notExported(i) + 1;
}

function notExported(i) {
   return i*2;
}

if (process.env.NODE_ENV === "test") {
   exports.notExported = notExported;
}
exports.exported = exported;

คำตอบอื่น ๆ ที่แนะนำให้ใช้โมดูล vm เพื่อประเมินไฟล์ แต่ไม่ได้ผลและมีข้อผิดพลาดที่ระบุว่าการส่งออกไม่ได้ถูกกำหนดไว้


8
ดูเหมือนว่าแฮ็คจะไม่มีวิธีทดสอบฟังก์ชั่นภายใน (ไม่ส่งออก) โดยไม่ทำเช่นนั้นหากบล็อก NODE_ENV หรือไม่
RyanHirsch

2
มันค่อนข้างน่ารังเกียจ นี่ไม่ใช่วิธีที่ดีที่สุดในการแก้ปัญหานี้
npiv

7

แก้ไข:

การโหลดโมดูลที่ใช้vmอาจทำให้เกิดพฤติกรรมที่ไม่คาดคิด (เช่นinstanceofผู้ปฏิบัติงานไม่สามารถทำงานกับวัตถุที่สร้างขึ้นในโมดูลดังกล่าวได้อีกต่อไปเพราะต้นแบบระดับโลกนั้นแตกต่างจากต้นแบบที่ใช้ในโมดูลที่โหลดตามปกติrequire) ฉันไม่ได้ใช้เทคนิคด้านล่างอีกต่อไปและใช้โมดูลrewireแทน มันใช้งานได้อย่างยอดเยี่ยม นี่คือคำตอบเดิมของฉัน:

อธิบายอย่างละเอียดเกี่ยวกับคำตอบของ srosh ...

รู้สึกแฮ็คเล็กน้อย แต่ฉันเขียนโมดูล "test_utils.js" ที่ควรอนุญาตให้คุณทำสิ่งที่คุณต้องการโดยไม่ต้องส่งออกตามเงื่อนไขในโมดูลแอปพลิเคชันของคุณ:

var Script = require('vm').Script,
    fs     = require('fs'),
    path   = require('path'),
    mod    = require('module');

exports.expose = function(filePath) {
  filePath = path.resolve(__dirname, filePath);
  var src = fs.readFileSync(filePath, 'utf8');
  var context = {
    parent: module.parent, paths: module.paths, 
    console: console, exports: {}};
  context.module = context;
  context.require = function (file){
    return mod.prototype.require.call(context, file);};
  (new Script(src)).runInNewContext(context);
  return context;};

มีบางสิ่งเพิ่มเติมที่รวมอยู่ในmoduleวัตถุgobal ของโหนดโมดูลที่อาจต้องเข้าสู่contextวัตถุด้านบนด้วย แต่นี่เป็นชุดขั้นต่ำที่ฉันต้องการเพื่อให้ทำงานได้

นี่คือตัวอย่างการใช้มอคค่า BDD:

var util   = require('./test_utils.js'),
    assert = require('assert');

var appModule = util.expose('/path/to/module/modName.js');

describe('appModule', function(){
  it('should test notExposed', function(){
    assert.equal(6, appModule.notExported(3));
  });
});

2
คุณสามารถยกตัวอย่างวิธีการที่คุณใช้ฟังก์ชั่นที่rewireไม่ได้ถูกเอ็กซ์พอร์ตได้อย่างไร?
Matthias

1
เฮ้แมทเทียสฉันยกตัวอย่างให้คุณทำอย่างนั้นในคำตอบของฉัน ถ้าคุณชอบมันอาจจะตอบคำถามของฉันได้ไหม :) คำถามของฉันเกือบทั้งหมดนั่งที่ 0 และ StackOverflow กำลังคิดเกี่ยวกับการแช่แข็งคำถามของฉัน X_X
แอนโทนี่

2

การทำงานกับจัสมินผมพยายามที่จะไปลึกกับโซลูชั่นที่นำเสนอโดยแอนโธนี Mayfieldบนพื้นฐานReWire

ฉันใช้งานฟังก์ชั่นต่อไปนี้( ข้อควรระวัง : ยังไม่ได้ทดสอบอย่างละเอียดเพียงแชร์เป็นกลยุทธ์ที่เป็นไปได้) :

function spyOnRewired() {
    const SPY_OBJECT = "rewired"; // choose preferred name for holder object
    var wiredModule = arguments[0];
    var mockField = arguments[1];

    wiredModule[SPY_OBJECT] = wiredModule[SPY_OBJECT] || {};
    if (wiredModule[SPY_OBJECT][mockField]) // if it was already spied on...
        // ...reset to the value reverted by jasmine
        wiredModule.__set__(mockField, wiredModule[SPY_OBJECT][mockField]);
    else
        wiredModule[SPY_OBJECT][mockField] = wiredModule.__get__(mockField);

    if (arguments.length == 2) { // top level function
        var returnedSpy = spyOn(wiredModule[SPY_OBJECT], mockField);
        wiredModule.__set__(mockField, wiredModule[SPY_OBJECT][mockField]);
        return returnedSpy;
    } else if (arguments.length == 3) { // method
        var wiredMethod = arguments[2];

        return spyOn(wiredModule[SPY_OBJECT][mockField], wiredMethod);
    }
}

ด้วยฟังก์ชันเช่นนี้คุณสามารถสอดแนมทั้งสองวิธีของวัตถุที่ไม่ถูกเอ็กซ์พอร์ตและฟังก์ชันระดับบนสุดที่ไม่ถูกเอ็กซ์พอร์ตดังนี้:

var dbLoader = require("rewire")("../lib/db-loader");
// Example: rewired module dbLoader
// It has non-exported, top level object 'fs' and function 'message'

spyOnRewired(dbLoader, "fs", "readFileSync").and.returnValue(FULL_POST_TEXT); // method
spyOnRewired(dbLoader, "message"); // top level function

จากนั้นคุณสามารถตั้งค่าความคาดหวังเช่นนี้:

expect(dbLoader.rewired.fs.readFileSync).toHaveBeenCalled();
expect(dbLoader.rewired.message).toHaveBeenCalledWith(POST_DESCRIPTION);

0

คุณสามารถสร้างบริบทใหม่โดยใช้โมดูลvmและประเมินไฟล์ js ในนั้นเรียงลำดับเหมือน repl ทำ จากนั้นคุณสามารถเข้าถึงทุกสิ่งที่ประกาศ


0

ฉันได้พบวิธีที่ง่ายมากที่ให้คุณทดสอบสอดแนมและเยาะเย้ยฟังก์ชั่นภายในเหล่านั้นจากภายในการทดสอบ:

สมมติว่าเรามีโมดูลโหนดดังนี้:

mymodule.js:
------------
"use strict";

function myInternalFn() {

}

function myExportableFn() {
    myInternalFn();   
}

exports.myExportableFn = myExportableFn;

ถ้าตอนนี้เราต้องการทดสอบและสอดแนมและเยาะเย้ยmyInternalFn ในขณะที่ไม่ส่งออกมันในการผลิตเราจะต้องปรับปรุงไฟล์ดังนี้:

my_modified_module.js:
----------------------
"use strict";

var testable;                          // <-- this is new

function myInternalFn() {

}

function myExportableFn() {
    testable.myInternalFn();           // <-- this has changed
}

exports.myExportableFn = myExportableFn;

                                       // the following part is new
if( typeof jasmine !== "undefined" ) {
    testable = exports;
} else {
    testable = {};
}

testable.myInternalFn = myInternalFn;

ตอนนี้คุณสามารถทดสอบสายลับและเยาะเย้ยmyInternalFnทุกที่ที่คุณใช้มันเป็นtestable.myInternalFnและในการผลิตมันจะไม่ถูกส่งออก


0

นี่ไม่ใช่วิธีปฏิบัติที่แนะนำ แต่ถ้าคุณไม่สามารถใช้rewireตามที่แนะนำโดย @Antoine คุณสามารถอ่านไฟล์และใช้งานeval()ได้เสมอ

var fs = require('fs');
const JsFileString = fs.readFileSync(fileAbsolutePath, 'utf-8');
eval(JsFileString);

ฉันพบว่ามีประโยชน์ในขณะที่หน่วยทดสอบไฟล์ JS ฝั่งไคลเอ็นต์สำหรับระบบดั้งเดิม

ไฟล์ JS จะตั้งค่าตัวแปรส่วนกลางจำนวนมากภายใต้windowไม่มีrequire(...)และmodule.exportsคำสั่ง (ไม่มีโมดูลบันเดิลเช่น Webpack หรือ Browserify พร้อมที่จะลบคำสั่งเหล่านี้ต่อไป)

แทนที่จะทำการปรับเปลี่ยน codebase ทั้งหมดสิ่งนี้ทำให้เราสามารถรวมการทดสอบหน่วยใน JS ฝั่งไคลเอ็นต์ของเรา

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.