Làm cách nào để kiểm tra đơn vị mô-đun Node.js yêu cầu các mô-đun khác và cách mô phỏng chức năng yêu cầu toàn cầu?


156

Đây là một ví dụ tầm thường minh họa mấu chốt của vấn đề của tôi:

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

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

module.exports = underTest;

Tôi đang cố gắng viết một bài kiểm tra đơn vị cho mã này. Làm thế nào tôi có thể giả định yêu cầu cho innerLibmà không chế nhạo requirechức năng hoàn toàn?

Vì vậy, đây là tôi đang cố gắng chế giễu toàn cầu requirevà phát hiện ra rằng nó sẽ không hoạt động ngay cả để làm điều đó:

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;
    }
};

Vấn đề là requirechức năng bên trong underTest.jstập tin thực sự chưa bị chế giễu. Nó vẫn chỉ đến requirechức năng toàn cầu . Vì vậy, có vẻ như tôi chỉ có thể giả lập requirechức năng trong cùng một tệp Tôi đang thực hiện chế độ giả. Nếu tôi sử dụng toàn cầu requiređể bao gồm mọi thứ, ngay cả khi tôi đã ghi đè lên bản sao cục bộ, các tệp được yêu cầu vẫn sẽ có requiretài liệu tham khảo toàn cầu .


bạn phải ghi đè lên global.require. Các biến được ghi moduletheo mặc định vì các mô-đun nằm trong phạm vi mô-đun.
Raynos

@Raynos Tôi sẽ làm điều đó như thế nào? global.require là không xác định? Ngay cả khi tôi thay thế nó bằng chức năng của mình, các chức năng khác sẽ không bao giờ sử dụng?
HMR

Câu trả lời:


174

Bây giờ bạn có thể!

Tôi đã xuất bản proxyquire , nó sẽ đảm nhiệm việc ghi đè yêu cầu toàn cầu bên trong mô-đun của bạn trong khi bạn đang thử nghiệm nó.

Điều này có nghĩa là bạn không cần thay đổi mã của mình để tiêm giả cho các mô-đun được yêu cầu.

Proxyquire có một api rất đơn giản cho phép giải quyết mô-đun mà bạn đang cố kiểm tra và chuyển qua các mô phỏng / sơ khai cho các mô-đun cần thiết trong một bước đơn giản.

@Raynos đúng là theo truyền thống, bạn phải sử dụng các giải pháp không lý tưởng để đạt được điều đó hoặc thực hiện phát triển từ dưới lên

Đó là lý do chính tại sao tôi tạo proxyquire - để cho phép phát triển theo hướng kiểm tra từ trên xuống mà không gặp rắc rối nào.

Hãy xem tài liệu và các ví dụ để đánh giá xem nó có phù hợp với nhu cầu của bạn không.


5
Tôi sử dụng proxyquire và tôi không thể nói đủ điều tốt. Nó đã cứu tôi! Tôi được giao nhiệm vụ viết các bài kiểm tra nút hoa nhài cho một ứng dụng được phát triển trong Titanium appcelerator, điều này buộc một số mô-đun phải là đường dẫn tuyệt đối và nhiều phụ thuộc vòng tròn. proxyquire cho phép tôi ngăn chặn những khoảng cách đó và chế nhạo hành trình mà tôi không cần cho mỗi bài kiểm tra. (Giải thích tại đây ). Cảm ơn bạn rất nhiều!
Sukima

Rất vui khi biết rằng proxyquire đã giúp bạn kiểm tra mã của mình đúng cách :)
Thorsten Lorenz

1
rất hay @ThorstenLorenz, tôi sẽ def. được sử dụng proxyquire!
bevacqua

Tuyệt diệu! Khi tôi thấy câu trả lời được chấp nhận rằng "bạn không thể" tôi đã nghĩ "Ôi Chúa ơi, nghiêm túc?!" nhưng điều này thực sự đã cứu nó.
Chadwick

3
Đối với những người sử dụng Webpack, đừng dành thời gian nghiên cứu proxyquire. Nó không hỗ trợ Webpack. Thay vào đó, tôi đang xem xét trình tải tiêm ( github.com/plasticine/inject-loader ).
Artif3x

116

Một lựa chọn tốt hơn trong trường hợp này là mô phỏng các phương thức của mô-đun được trả về.

Để tốt hơn hoặc tồi tệ hơn, hầu hết các mô-đun node.js là singletons; hai đoạn mã yêu cầu () cùng một mô-đun có cùng tham chiếu đến mô-đun đó.

Bạn có thể tận dụng điều này và sử dụng một cái gì đó như sinon để chế nhạo các vật phẩm được yêu cầu. thử nghiệm mocha sau:

// 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 có khả năng tích hợp tốt với chai để đưa ra các xác nhận và tôi đã viết một mô-đun để tích hợp sinon với mocha để cho phép dọn dẹp gián điệp / sơ khai dễ dàng hơn (để tránh ô nhiễm thử nghiệm.)

Lưu ý rằng underTest không thể được chế giễu theo cùng một cách, vì underTest chỉ trả về một hàm.

Một lựa chọn khác là sử dụng giả Jest. Theo dõi trên trang của họ


1
Thật không may, các mô-đun node.js KHÔNG được đảm bảo là singletons, như được giải thích ở đây: justjs.com/posts/iêu
FrontierPologistso

4
@FrontierPologistso một vài điều: Đầu tiên, liên quan đến thử nghiệm, bài viết không liên quan. Miễn là bạn đang kiểm tra các phụ thuộc của mình (chứ không phải phụ thuộc vào các phụ thuộc), tất cả mã của bạn sẽ lấy lại cùng một đối tượng khi bạn require('some_module'), bởi vì tất cả các mã của bạn đều chia sẻ cùng một dir_modules dir. Thứ hai, bài viết đang kết hợp không gian tên với singletons, đó là loại trực giao. Thứ ba, bài viết đó khá cũ kỹ (liên quan đến node.js), vì vậy những gì có thể có giá trị trở lại trong ngày hiện tại có thể không hợp lệ.
Elliot Foster

2
Hừm. Trừ khi một trong số chúng tôi thực sự đào mã chứng minh điểm này hay điểm khác, tôi sẽ sử dụng giải pháp tiêm phụ thuộc của bạn, hoặc chỉ đơn giản là chuyển các đối tượng xung quanh, bằng chứng an toàn hơn và tương lai hơn.
FrontierPologistso

1
Tôi không chắc chắn những gì bạn yêu cầu được chứng minh. Bản chất singleton (lưu trữ) của các mô-đun nút thường được hiểu. Phụ thuộc tiêm, trong khi một tuyến đường tốt, có thể là một số lượng khá nhiều tấm nồi hơi và nhiều mã hơn. DI là phổ biến hơn trong các ngôn ngữ được nhập tĩnh, trong đó khó khăn hơn để gián điệp đấm / cùi / giả vào mã của bạn một cách linh hoạt. Nhiều dự án mà tôi đã thực hiện trong ba năm qua sử dụng phương pháp được mô tả trong câu trả lời của tôi ở trên. Đó là cách dễ nhất trong tất cả các phương pháp, mặc dù tôi sử dụng nó một cách tiết kiệm.
Elliot Foster

1
Tôi đề nghị bạn đọc lên sinon.js. Nếu bạn đang sử dụng sinon (như trong ví dụ ở trên), bạn sẽ hoặc innerLib.toCrazyCrap.restore()đặt lại hoặc gọi sinon qua sinon.stub(innerLib, 'toCrazyCrap')đó cho phép bạn thay đổi cách thức sơ khai hành xử : innerLib.toCrazyCrap.returns(false). Ngoài ra, tua lại dường như rất giống với proxyquirephần mở rộng ở trên.
Elliot Foster

11

Tôi sử dụng mock-Yêu cầu . Hãy chắc chắn rằng bạn xác định giả của mình trước khi requiremô-đun được kiểm tra.


Cũng tốt để thực hiện dừng (<file>) hoặc stop ALL () ngay lập tức để bạn không nhận được tệp được lưu trong bộ nhớ cache trong bài kiểm tra mà bạn không muốn giả.
Justin Kruse

1
Điều này đã giúp một tấn.
wallop

2

Mocking requirecảm thấy như một hack khó chịu với tôi. Cá nhân tôi sẽ cố gắng tránh nó và cấu trúc lại mã để làm cho nó dễ kiểm tra hơn. Có nhiều cách tiếp cận khác nhau để xử lý các phụ thuộc.

1) vượt qua phụ thuộc làm đối số

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

Điều này sẽ làm cho mã phổ biến có thể kiểm tra. Nhược điểm là bạn cần vượt qua các phụ thuộc xung quanh, điều này có thể làm cho mã trông phức tạp hơn.

2) triển khai mô đun như một lớp, sau đó sử dụng các phương thức / thuộc tính của lớp để có được các phụ thuộc

(Đây là một ví dụ giả định, trong đó việc sử dụng lớp không hợp lý, nhưng nó truyền đạt ý tưởng) (ví dụ ES6)

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

class underTestClass {
    getInnerLib () {
        return innerLib
    }

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

Bây giờ bạn có thể dễ dàng sơ khai getInnerLibphương pháp để kiểm tra mã của bạn. Mã trở nên dài dòng hơn, nhưng cũng dễ kiểm tra hơn.


1
Tôi không nghĩ đó là hacky như bạn đoán ... đây là bản chất của chế giễu. Mocking phụ thuộc cần thiết làm cho mọi thứ đơn giản đến mức nó cung cấp quyền kiểm soát cho nhà phát triển mà không thay đổi cấu trúc mã. Phương pháp của bạn quá dài dòng và do đó khó lý luận. Tôi chọn proxyrequire hoặc giả-yêu cầu về điều này; Tôi không thấy bất kỳ vấn đề ở đây. Mã sạch sẽ và dễ dàng để lý luận và nhớ hầu hết những người đọc mã này đã có mã viết mà bạn muốn họ phức tạp. Nếu những lib này là hackish, thì việc chế giễu và stubbing cũng bị hack theo định nghĩa của bạn & nên được dừng lại.
Emmanuel Mahuni

1
Vấn đề với cách tiếp cận số 1 là bạn đang chuyển chi tiết triển khai nội bộ lên ngăn xếp. Với nhiều lớp, việc trở thành người tiêu dùng mô-đun của bạn trở nên phức tạp hơn nhiều. Nó có thể hoạt động với phương thức IOC như cách tiếp cận để các phụ thuộc được tự động chèn cho bạn, tuy nhiên cảm giác như chúng ta đã có các phụ thuộc được tiêm trong các mô-đun nút thông qua câu lệnh nhập khẩu, nên có thể giả định chúng ở mức đó .
magritte

1) Điều này chỉ đơn giản là chuyển vấn đề sang tệp khác 2) vẫn tải mô-đun khác và do đó áp đặt chi phí hiệu năng và có thể gây ra tác dụng phụ (như colorsmô-đun phổ biến gây rối String.prototype)
ThomasR

2

Nếu bạn đã từng sử dụng jest, thì có lẽ bạn đã quen thuộc với tính năng giả của jest.

Sử dụng "jest.mock (...)", bạn có thể chỉ định chuỗi sẽ xảy ra trong câu lệnh request trong mã của mình ở đâu đó và bất cứ khi nào mô-đun được yêu cầu sử dụng chuỗi đó, đối tượng giả sẽ được trả về.

Ví dụ

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

sẽ thay thế hoàn toàn tất cả các lần nhập / yêu cầu của "firebase-admin" bằng đối tượng bạn đã trả về từ chức năng "nhà máy" đó.

Chà, bạn có thể làm điều đó khi sử dụng jest vì jest tạo thời gian chạy xung quanh mỗi mô-đun mà nó chạy và đưa một phiên bản yêu cầu "nối" vào mô-đun, nhưng bạn sẽ không thể làm điều này mà không có jest.

Tôi đã cố gắng để đạt được điều này với yêu cầu giả nhưng đối với tôi nó không hoạt động đối với các mức lồng nhau trong nguồn của tôi. Hãy xem vấn đề sau trên github: mock-request không phải lúc nào cũng được gọi bằng Mocha .

Để giải quyết điều này tôi đã tạo ra hai mô-đun npm bạn có thể sử dụng để đạt được những gì bạn muốn.

Bạn cần một plugin babel và một trình mô phỏng mô-đun.

Trong .babelrc của bạn, hãy sử dụng plugin babel-plugin-mock-Yêu cầu với các tùy chọn sau:

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

và trong tệp thử nghiệm của bạn sử dụng mô-đun jestlike-mock như vậy:

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

Các jestlike-mockmô-đun vẫn còn rất thô sơ và không có nhiều tài liệu nhưng không có nhiều mã trong hai. Tôi đánh giá cao bất kỳ PR cho một bộ tính năng đầy đủ hơn. Mục tiêu sẽ là tạo lại toàn bộ tính năng "jest.mock".

Để xem cách jest thực hiện mà người ta có thể tra cứu mã trong gói "jest-runtime". Xem https://github.com/facebook/jest/blob/master/packages/jest-r nb / src / index.js # L734, ví dụ, ở đây họ tạo ra "automock" của mô-đun.

Hy vọng rằng sẽ giúp;)


1

Bạn không thể. Bạn phải xây dựng bộ kiểm thử đơn vị của mình để các mô-đun thấp nhất được kiểm tra trước và các mô-đun cấp cao hơn yêu cầu các mô-đun được kiểm tra sau đó.

Bạn cũng phải giả sử rằng bất kỳ mã bên thứ 3 và chính node.js nào cũng được kiểm tra.

Tôi đoán bạn sẽ thấy các khung mô phỏng đến trong tương lai gần sẽ ghi đè lên global.require

Nếu bạn thực sự phải tiêm giả, bạn có thể thay đổi mã của mình để hiển thị phạm vi mô-đun.

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

Được cảnh báo điều này phơi bày .__moduletrong API của bạn và bất kỳ mã nào cũng có thể truy cập phạm vi mô-đun một cách nguy hiểm.


2
Giả sử rằng mã của bên thứ ba được kiểm tra tốt không phải là cách tuyệt vời để làm việc IMO.
henry.oswald

5
@block đó là một cách tuyệt vời để làm việc. Nó buộc bạn chỉ làm việc với mã bên thứ ba chất lượng cao hoặc viết tất cả các đoạn mã của bạn để mọi phụ thuộc đều được kiểm tra tốt
Raynos

Ok tôi nghĩ rằng bạn đang đề cập đến việc không thực hiện các thử nghiệm tích hợp giữa mã của bạn và mã của bên thứ ba. Đã đồng ý.
henry.oswald

1
"Bộ kiểm thử đơn vị" chỉ là một tập hợp các bài kiểm tra đơn vị, nhưng các bài kiểm tra đơn vị phải độc lập với nhau, do đó đơn vị trong bài kiểm tra đơn vị. Để có thể sử dụng, các bài kiểm tra đơn vị phải nhanh và độc lập, để bạn có thể thấy rõ mã bị hỏng ở đâu khi kiểm tra đơn vị thất bại.
Andreas Berheim Brudin

Điều này đã không làm việc cho tôi. Đối tượng mô-đun không hiển thị "var InternalLib ...", v.v.
AnitKstall

1

Bạn có thể sử dụng thư viện nhạo báng :

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

Mã đơn giản để mô phỏng các mô-đun cho người tò mò

Lưu ý các phần mà bạn thao tác require.cachevà ghi chú require.resolvephương pháp vì đây là nước sốt bí mật.

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 = {}
  }
}

Sử dụng như :

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

NHƯNG ... proxyquire khá tuyệt vời và bạn nên sử dụng nó. Nó giữ yêu cầu của bạn ghi đè cục bộ chỉ để kiểm tra và tôi rất khuyến khích điều đó.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.