วิธีทดสอบยูนิต Node.js ที่ต้องใช้โมดูลอื่นและวิธีการจำลองฟังก์ชันการทำงานแบบโกลบอล


156

นี่เป็นตัวอย่างเล็กน้อยที่แสดงให้เห็นถึงปมปัญหาของฉัน:

var innerLib = require('./path/to/innerLib');

function underTest() {
    return innerLib.doComplexStuff();
}

module.exports = underTest;

ฉันพยายามเขียนการทดสอบหน่วยสำหรับรหัสนี้ ฉันจะเยาะเย้ยความต้องการสำหรับinnerLibโดยไม่ต้องเยาะเย้ยrequireฟังก์ชั่นทั้งหมดได้อย่างไร

ดังนั้นนี่คือฉันพยายามที่จะเยาะเย้ยทั่วโลกrequireและพบว่ามันจะไม่ทำงานแม้จะทำเช่นนั้น:

var path = require('path'),
    vm = require('vm'),
    fs = require('fs'),
    indexPath = path.join(__dirname, './underTest');

var globalRequire = require;

require = function(name) {
    console.log('require: ' + name);
    switch(name) {
        case 'connect':
        case indexPath:
            return globalRequire(name);
            break;
    }
};

ปัญหาคือว่าrequireฟังก์ชั่นภายในunderTest.jsไฟล์ไม่ได้ถูกเยาะเย้ย มันยังคงชี้ไปที่requireฟังก์ชั่นระดับโลก ดังนั้นดูเหมือนว่าฉันสามารถเลียนแบบrequireฟังก์ชั่นภายในไฟล์เดียวกับที่ฉันกำลังจำลองอยู่เท่านั้นหากฉันใช้โกลบอลrequireเพื่อรวมสิ่งต่าง ๆ แม้หลังจากที่ฉันเขียนทับโลคัลแล้วไฟล์ที่ต้องการจะยังคงมีrequireการอ้างอิงระดับโลก


global.requireคุณต้องเขียนทับ ตัวแปรที่เขียนถึงmoduleเป็นค่าเริ่มต้นเนื่องจากโมดูลเป็นโมดูลที่กำหนดขอบเขต
Raynos

@ Raynos ฉันจะทำอย่างไร global.require ไม่ได้ถูกกำหนดไว้? แม้ว่าฉันจะแทนที่ด้วยฟังก์ชั่นของตัวเองฟังก์ชั่นอื่น ๆ จะไม่ใช้สิ่งที่พวกเขาจะ?
HMR

คำตอบ:


175

ตอนนี้คุณสามารถ!

ฉันเผยแพร่proxyquireซึ่งจะดูแลเอาชนะความต้องการทั่วโลกในโมดูลของคุณในขณะที่คุณกำลังทดสอบ

ซึ่งหมายความว่าคุณไม่จำเป็นต้องเปลี่ยนแปลงโค้ดเพื่อฉีด mocks สำหรับโมดูลที่ต้องการ

Proxyquire มี API ที่ง่ายมากซึ่งช่วยให้การแก้ไขโมดูลที่คุณพยายามทดสอบและส่งผ่าน mocks / stubs สำหรับโมดูลที่ต้องการในขั้นตอนเดียว

@Raynos นั้นถูกต้องตามธรรมเนียมคุณต้องใช้วิธีการแก้ปัญหาที่ไม่สมบูรณ์แบบเพื่อให้บรรลุหรือพัฒนาจากล่างขึ้นบน

ซึ่งเป็นเหตุผลหลักว่าทำไมฉันสร้าง proxyquire - เพื่อให้การพัฒนาแบบทดสอบจากบนลงล่างโดยไม่ต้องยุ่งยาก

ดูเอกสารและตัวอย่างเพื่อวัดว่าเหมาะสมกับความต้องการของคุณหรือไม่


5
ฉันใช้ proxyquire และฉันไม่สามารถพูดสิ่งที่ดีพอ มันช่วยฉันได้! ฉันได้รับมอบหมายให้เขียนการทดสอบจัสมินโหนดสำหรับแอพที่พัฒนาขึ้นใน appcelerator Titanium ซึ่งบังคับให้โมดูลบางอย่างเป็นเส้นทางที่แน่นอนและการพึ่งพาแบบวงกลมจำนวนมาก proxyquire ให้ฉันหยุดช่องว่างเหล่านั้นและเยาะเย้ย cruft ที่ฉันไม่ต้องการสำหรับการทดสอบแต่ละครั้ง (อธิบายที่นี่ ) ขอบคุณมาก ๆ !
Sukima

ยินดีที่จะได้ยิน proxyquire ที่ช่วยให้คุณทดสอบรหัสของคุณอย่างถูกต้อง :)
Thorsten อเรนซ์

1
ดีมาก @ThorstenLorenz ฉันจะป้องกัน ใช้proxyquire!
bevacqua

Fantastic! เมื่อฉันเห็นคำตอบที่ยอมรับได้ว่า "คุณไม่สามารถ" ฉันคิดว่า "โอ้พระเจ้าจริงเหรอ?!" แต่สิ่งนี้ช่วยได้จริงๆ
Chadwick

3
สำหรับผู้ที่ใช้ Webpack อย่าใช้เวลาค้นคว้า proxyquire ไม่รองรับ Webpack ฉันกำลังมองหา inject-loader แทน ( github.com/plasticine/inject-loader )
Artif3x

116

ตัวเลือกที่ดีกว่าในกรณีนี้คือการจำลองวิธีการของโมดูลที่ได้รับคืน

เพื่อให้ดีขึ้นหรือแย่ลงโมดูล node.js ส่วนใหญ่เป็นซิงเกิลตัน โค้ดสองชิ้นที่ต้องการ () โมดูลเดียวกันได้รับการอ้างอิงเดียวกันกับโมดูลนั้น

คุณสามารถใช้ประโยชน์จากสิ่งนี้และใช้บางอย่างเช่นsinonเพื่อจำลองรายการที่จำเป็น การทดสอบมอคค่าดังนี้:

// in your testfile
var innerLib  = require('./path/to/innerLib');
var underTest = require('./path/to/underTest');
var sinon     = require('sinon');

describe("underTest", function() {
  it("does something", function() {
    sinon.stub(innerLib, 'toCrazyCrap').callsFake(function() {
      // whatever you would like innerLib.toCrazyCrap to do under test
    });

    underTest();

    sinon.assert.calledOnce(innerLib.toCrazyCrap); // sinon assertion

    innerLib.toCrazyCrap.restore(); // restore original functionality
  });
});

Sinon มีการบูรณาการที่ดีกับชัยเพื่อทำการยืนยันและฉันเขียนโมดูลเพื่อรวม Sinon กับมอคค่าเพื่อให้ง่ายต่อการล้างข้อมูลสายลับ / ต้นขั้ว (เพื่อหลีกเลี่ยงการทดสอบมลพิษ)

โปรดทราบว่าภายใต้การทดสอบไม่สามารถจำลองในลักษณะเดียวกันได้เนื่องจาก underTest จะส่งกลับเฉพาะฟังก์ชัน

ตัวเลือกอื่นคือการใช้ Jocks mocks ติดตามบนหน้าของพวกเขา


1
น่าเสียดายที่โมดูล node.js ไม่ได้รับประกันว่าจะเป็นซิงเกิลตันตามที่อธิบายไว้ที่นี่: justjs.com/posts/…
FrontierPsycho

4
@ FrontierPsycho บางสิ่ง: อันดับแรกเท่าที่เกี่ยวข้องกับการทดสอบบทความไม่เกี่ยวข้อง ตราบใดที่คุณกำลังทดสอบการขึ้นต่อกันของคุณ (และไม่ใช่การขึ้นต่อกันของการพึ่งพา) โค้ดทั้งหมดของคุณจะได้รับออบเจ็กต์เดียวกันเมื่อคุณrequire('some_module')เพราะรหัสทั้งหมดของคุณใช้ร่วมกัน node_modules dir ประการที่สองบทความกำลังทำ namespace ให้สมกับ singletons ซึ่งเป็น orthogonal ประการที่สามบทความนั้นค่อนข้างเก่า (เท่าที่เกี่ยวข้องกับ node.js) ดังนั้นสิ่งที่อาจใช้ได้ในวันนี้อาจไม่ถูกต้องในขณะนี้
Elliot Foster

2
ฮึ่ม หากเราไม่ขุดค้นรหัสที่พิสูจน์จุดหนึ่งหรืออีกอันหนึ่งฉันจะไปหาทางออกของการฉีดพึ่งพาหรือเพียงแค่ส่งวัตถุไปรอบ ๆ มันปลอดภัยและเป็นหลักฐานในอนาคตมากขึ้น
FrontierPsycho

1
ฉันไม่แน่ใจว่าสิ่งที่คุณขอให้ได้รับการพิสูจน์ ความเข้าใจทั่วไปของซิงเกิล (แคช) ของโมดูลโหนด การฉีดพึ่งพาในขณะที่เส้นทางที่ดีสามารถเป็นจำนวนหม้อไอน้ำที่เป็นธรรมมากขึ้นและรหัสมากขึ้น DI นั้นพบได้ทั่วไปในภาษาที่พิมพ์แบบคงที่ซึ่งยากต่อการเจาะสายลับ / สตับ / mocks ลงในโค้ดของคุณแบบไดนามิก หลายโครงการที่ฉันทำในช่วงสามปีที่ผ่านมาใช้วิธีการที่อธิบายไว้ในคำตอบของฉันข้างต้น มันเป็นวิธีที่ง่ายที่สุดในทุกวิธีแม้ว่าฉันจะใช้เท่าที่จำเป็น
Elliot Foster

1
ฉันขอแนะนำให้คุณอ่านบน sinon.js หากคุณกำลังใช้ Sinon (เช่นในตัวอย่างข้างต้น) ที่คุณต้องการอย่างใดอย่างหนึ่งinnerLib.toCrazyCrap.restore()และ restub หรือโทร Sinon ผ่านซึ่งจะช่วยให้คุณสามารถเปลี่ยนวิธีการที่จะทำงานแบบสั้น:sinon.stub(innerLib, 'toCrazyCrap') innerLib.toCrazyCrap.returns(false)ยิ่งไปกว่านั้น rewire ดูเหมือนจะเหมือนกับproxyquireส่วนขยายด้านบนอย่างมาก
Elliot Foster

11

ผมใช้จำลองต้อง ตรวจสอบให้แน่ใจว่าคุณกำหนด mocks ของคุณก่อนrequireโมดูลที่จะทดสอบ


นอกจากนี้ยังเป็นการดีที่จะหยุด (<file>) หรือ stopAll () ทันทีเพื่อให้คุณไม่ได้รับไฟล์แคชในการทดสอบที่คุณไม่ต้องการจำลอง
Justin Kruse

1
สิ่งนี้ช่วยได้มาก
wallop

2

การเยาะเย้ยrequireรู้สึกเหมือนแฮ็คที่น่ารังเกียจสำหรับฉัน โดยส่วนตัวฉันจะพยายามหลีกเลี่ยงมันและสร้างรหัสใหม่เพื่อให้สามารถทดสอบได้มากขึ้น มีวิธีการต่างๆในการจัดการการพึ่งพา

1) ผ่านการอ้างอิงเป็นอาร์กิวเมนต์

function underTest(innerLib) {
    return innerLib.doComplexStuff();
}

สิ่งนี้จะทำให้โค้ดทดสอบได้ในระดับสากล ข้อเสียคือคุณต้องผ่านการอ้างอิงรอบ ๆ ซึ่งจะทำให้โค้ดดูซับซ้อนมากขึ้น

2) ใช้โมดูลเป็นคลาสจากนั้นใช้เมธอด / คุณสมบัติคลาสเพื่อรับการพึ่งพา

(นี่คือตัวอย่างที่วางแผนไว้โดยที่การใช้คลาสไม่สมเหตุสมผล แต่สื่อถึงแนวคิด) (ตัวอย่าง ES6)

const innerLib = require('./path/to/innerLib')

class underTestClass {
    getInnerLib () {
        return innerLib
    }

    underTestMethod () {
        return this.getInnerLib().doComplexStuff()
    }
}

ตอนนี้คุณสามารถ stub getInnerLibวิธีทดสอบโค้ดของคุณได้อย่างง่ายดาย รหัสจะละเอียดยิ่งขึ้น แต่ยังง่ายต่อการทดสอบ


1
ฉันไม่คิดว่ามันจะแฮ็กในขณะที่คุณเข้าใจ ... นี่เป็นสาระสำคัญของการล้อเลียน การจำลองการพึ่งพาที่จำเป็นทำให้สิ่งต่าง ๆ ง่ายมากที่จะให้การควบคุมแก่นักพัฒนาโดยไม่ต้องเปลี่ยนโครงสร้างรหัส วิธีการของคุณละเอียดเกินไปและยากที่จะให้เหตุผล ฉันเลือก proxyrequire หรือ mock-need มากกว่านี้ ฉันไม่เห็นปัญหาใด ๆ ที่นี่ รหัสนั้นสะอาดและง่ายต่อการเข้าใจและจำได้ว่าคนส่วนใหญ่ที่อ่านข้อความนี้มีโค้ดที่คุณต้องการให้ซับซ้อน หาก libs เหล่านี้ถูกแฮ็กการเยาะเย้ยและการขัดถูก็เป็นการแฮกโดยนิยามของคุณและควรหยุด
Emmanuel Mahuni

1
ปัญหาของวิธีที่ 1 คือคุณกำลังส่งรายละเอียดการใช้งานภายในสแต็ก ด้วยเลเยอร์หลายเลเยอร์มันจะกลายเป็นเรื่องซับซ้อนมากขึ้นที่จะเป็นผู้บริโภคของโมดูลของคุณ มันสามารถทำงานร่วมกับ IOC container เหมือนกับวิธีการ แต่เพื่อให้การพึ่งพานั้นถูกฉีดเข้ามาให้คุณโดยอัตโนมัติอย่างไรก็ตามมันให้ความรู้สึกเหมือนว่าเรามีการพึ่งพาการฉีดเข้าไปในโมดูลโหนดผ่านทางคำสั่ง import ดังนั้นจึงเหมาะสมที่จะเยาะเย้ย .
magritte

1) เพียงแค่ย้ายปัญหาไปยังไฟล์อื่น 2) ยังคงโหลดโมดูลอื่นและทำให้มีการใช้งานที่มีประสิทธิภาพและอาจทำให้เกิดผลข้างเคียง (เช่นcolorsโมดูลยอดนิยมที่String.prototype
ยุ่งเหยิง

2

หากคุณเคยใช้การเล่นตลกคุณอาจคุ้นเคยกับคุณสมบัติการล้อเลียนของตลก

การใช้ "jest.mock (... )" คุณสามารถระบุสตริงที่จะเกิดขึ้นในคำสั่ง require ในรหัสของคุณที่ใดที่หนึ่งและเมื่อใดก็ตามที่จำเป็นต้องใช้โมดูลโดยใช้สตริงนั้นจะมีการจำลองวัตถุจำลอง

ตัวอย่างเช่น

jest.mock("firebase-admin", () => {
    const a = require("mocked-version-of-firebase-admin");
    a.someAdditionalMockedMethod = () => {}
    return a;
})

จะแทนที่การนำเข้า / ต้องการทั้งหมดของ "firebase-admin" ด้วยวัตถุที่คุณส่งคืนจากฟังก์ชั่น "โรงงาน" นั้น

คุณสามารถทำได้เมื่อใช้ jest เพราะ jest สร้าง runtime รอบ ๆ โมดูลที่มันรันและ injects ต้องการ "hooked" version ในโมดูล แต่คุณจะไม่สามารถทำได้โดยไม่ล้อเล่น

ฉันพยายามที่จะบรรลุเป้าหมายนี้ด้วย จำลองแต่สำหรับฉันมันไม่ได้ผลกับระดับที่ซ้อนกันในแหล่งที่มาของฉัน มีลักษณะที่ปัญหาต่อไปนี้บน GitHub A: จำลองไม่จำเป็นต้องเรียกว่าเสมอกับมอคค่า

เพื่อแก้ไขปัญหานี้ฉันได้สร้างโมดูล npm สองโมดูลคุณสามารถใช้เพื่อบรรลุสิ่งที่คุณต้องการ

คุณต้องมี babel-plugin หนึ่งตัวและโมดูลผู้เยาะเย้ย

ใน. babelrc ของคุณให้ใช้ปลั๊กอิน babel-plugin-mock-require ด้วยตัวเลือกต่อไปนี้:

...
"plugins": [
        ["babel-plugin-mock-require", { "moduleMocker": "jestlike-mock" }],
        ...
]
...

และในไฟล์ทดสอบของคุณใช้โมดูล jestlike-mock ดังนี้:

import {jestMocker} from "jestlike-mock";
...
jestMocker.mock("firebase-admin", () => {
            const firebase = new (require("firebase-mock").MockFirebaseSdk)();
            ...
            return firebase;
});
...

jestlike-mockโมดูลยังคง Rudimental มากและไม่ได้มีเอกสารจำนวนมาก แต่มีไม่มากทั้งรหัส ฉันขอขอบคุณ PRs ใด ๆ สำหรับชุดคุณลักษณะที่สมบูรณ์ยิ่งขึ้น เป้าหมายคือการสร้างคุณสมบัติ "jest.mock" ใหม่ทั้งหมด

เพื่อดูว่า jest ใช้งานได้อย่างไรในการค้นหาโค้ดในแพ็คเกจ "jest-runtime" ดูhttps://github.com/facebook/jest/blob/master/packages/jest-runtime/src/index.js#L734ยกตัวอย่างเช่นที่นี่พวกเขาสร้าง "automock" ของโมดูล

หวังว่าจะช่วย;)


1

คุณทำไม่ได้ คุณต้องสร้างชุดทดสอบหน่วยของคุณเพื่อให้โมดูลต่ำสุดได้รับการทดสอบก่อนและโมดูลระดับสูงที่ต้องการโมดูลนั้นจะถูกทดสอบในภายหลัง

คุณต้องสมมติว่าโค้ดของบุคคลที่สามและ node.js นั้นผ่านการทดสอบเป็นอย่างดี

ฉันคิดว่าคุณจะเห็นกรอบการเยาะเย้ยมาถึงในอนาคตอันใกล้ที่เขียนทับ global.require

หากคุณต้องจำลองการเยาะเย้ยคุณสามารถเปลี่ยนรหัสของคุณเพื่อแสดงขอบเขตแบบแยกส่วนได้

// underTest.js
var innerLib = require('./path/to/innerLib');

function underTest() {
    return innerLib.toCrazyCrap();
}

module.exports = underTest;
module.exports.__module = module;

// test.js
function test() {
    var underTest = require("underTest");
    underTest.__module.innerLib = {
        toCrazyCrap: function() { return true; }
    };
    assert.ok(underTest());
}

ถูกเตือนว่าสิ่งนี้จะถูกเปิดเผย.__moduleใน API ของคุณและโค้ดใด ๆ ที่สามารถเข้าถึงขอบเขตแบบแยกส่วนได้ด้วยตนเอง


2
สมมติว่ารหัสบุคคลที่สามได้รับการทดสอบอย่างดีไม่ใช่วิธีที่ยอดเยี่ยมในการทำงาน IMO
henry.oswald

5
@ เบ็คมันเป็นวิธีที่ดีในการทำงาน มันบังคับให้คุณทำงานกับโค้ดของบุคคลที่สามที่มีคุณภาพสูงหรือเขียนโค้ดทั้งหมดของคุณเพื่อให้การอ้างอิงทุกครั้งผ่านการทดสอบเป็นอย่างดี
Raynos

ตกลงฉันคิดว่าคุณหมายถึงไม่ทำการทดสอบการรวมระหว่างรหัสของคุณและรหัสบุคคลที่สาม ตกลง
henry.oswald

1
"ชุดทดสอบหน่วย" เป็นเพียงชุดของการทดสอบหน่วย แต่การทดสอบหน่วยควรเป็นอิสระจากกันดังนั้นหน่วยในการทดสอบหน่วย เพื่อให้สามารถใช้งานได้การทดสอบแบบหน่วยจะต้องรวดเร็วและเป็นอิสระเพื่อให้คุณสามารถเห็นได้อย่างชัดเจนว่ารหัสถูกทำลายเมื่อการทดสอบหน่วยล้มเหลว
Andreas Berheim Brudin

สิ่งนี้ไม่ได้ผลสำหรับฉัน วัตถุโมดูลไม่เปิดเผย "var innerLib ... " ฯลฯ
AnitKryst

1

คุณสามารถใช้ห้องสมุดการเยาะเย้ย :

describe 'UnderTest', ->
  before ->
    mockery.enable( warnOnUnregistered: false )
    mockery.registerMock('./path/to/innerLib', { doComplexStuff: -> 'Complex result' })
    @underTest = require('./path/to/underTest')

  it 'should compute complex value', ->
    expect(@underTest()).to.eq 'Complex result'

1

รหัสง่าย ๆ ที่จะจำลองโมดูลสำหรับคนที่อยากรู้อยากเห็น

สังเกตุชิ้นส่วนที่คุณใช้วิธีการrequire.cacheและบันทึกย่อrequire.resolveเนื่องจากนี่เป็นซอสลับ

class MockModules {  
  constructor() {
    this._resolvedPaths = {} 
  }
  add({ path, mock }) {
    const resolvedPath = require.resolve(path)
    this._resolvedPaths[resolvedPath] = true
    require.cache[resolvedPath] = {
      id: resolvedPath,
      file: resolvedPath,
      loaded: true,
      exports: mock
    }
  }
  clear(path) {
    const resolvedPath = require.resolve(path)
    delete this._resolvedPaths[resolvedPath]
    delete require.cache[resolvedPath]
  }
  clearAll() {
    Object.keys(this._resolvedPaths).forEach(resolvedPath =>
      delete require.cache[resolvedPath]
    )
    this._resolvedPaths = {}
  }
}

ใช้เช่น :

describe('#someModuleUsingTheThing', () => {
  const mockModules = new MockModules()
  beforeAll(() => {
    mockModules.add({
      // use the same require path as you normally would
      path: '../theThing',
      // mock return an object with "theThingMethod"
      mock: {
        theThingMethod: () => true
      }
    })
  })
  afterAll(() => {
    mockModules.clearAll()
  })
  it('should do the thing', async () => {
    const someModuleUsingTheThing = require('./someModuleUsingTheThing')
    expect(someModuleUsingTheThing.theThingMethod()).to.equal(true)
  })
})

แต่ ... proxyquire นั้นยอดเยี่ยมมากและคุณควรใช้มัน มันทำให้ความต้องการของคุณแทนที่ภาษาท้องถิ่นเพื่อการทดสอบเท่านั้นและฉันขอแนะนำให้มัน

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