在函数中创建的 JavaScript 对象的模拟方法

Mocking methods of a JavaScript object created within a function

我写了一个 JavaScript 函数,它从 require() 的库中创建一个对象,然后使用它。当我尝试为它编写测试时,这似乎给我带来了麻烦,因为我似乎没有很好的方法来控制该对象并创建其方法的模拟来测试我的函数的行为。

我 运行 是不是因为我设计的功能不好?我来自 Java/Spring 背景,所以我脑子里的声音在尖叫“依赖注入”。有没有比将我的函数需要的对象作为参数传递给它更好的方法?

示例函数:

// dbService.js
const AWS = require('aws-sdk');

function getItem() {
    const dynamo = new AWS.DynamoDB.DocumentClient();
    var params = {/* irrelevant */}

    try {
        return await dynamo.get(getParams).promise();
    } catch (err) {
        return err;
    }
}

exports.getItem = getItem;

当我尝试编写测试来验证函数的行为时 dynamo.get() returns 成功或抛出错误时,我开始 运行 陷入困境。

示例测试(我一直使用 Sinon 进行模拟,使用 Chai 进行断言):

// dbServiceTest.js
const sinon = require('sinon');
const dbService = require('dbService.js');
const expect = require('chai').expect;

describe('dbService: When database returns a record', function() {
    let dbMock, dbServiceResp = null;

    beforeEach(async function() {
        dbMock = sinon.stub(dynamo, "get")
            .returns({Item: "an item"});
        dbServiceResp = await dbService.getItem("an item");
    });

    afterEach(function() {
        dbMock.restore();
    });

    it('Should have expected value', function() {
        expect(dbServiceResp.Item).to.be.equal("an item");
    });
});

很明显,我创建的 dynamo.get() 的模拟没有被 dbService.getItem() 使用,因为 dbService.getItem() 完全拥有它自己对 [= 的依赖的实例化17=]对象。

我应该只将 DocumentClient 传递到我的 getItem() 函数中,还是有更好的方法?

DI是最好的方式,它会让你的代码更容易测试,更好的扩展性,解耦模块。但是如果你想 require 模块作为依赖,你仍然可以存根 aws-sdk 模块。单元测试方案:

dbService.js:

const AWS = require('aws-sdk');

async function getItem() {
  const dynamo = new AWS.DynamoDB.DocumentClient();
  var params = {
    /* irrelevant */
  };

  try {
    return await dynamo.get(params).promise();
  } catch (err) {
    return err;
  }
}

exports.getItem = getItem;

dbService.test.js:

const sinon = require('sinon');
const AWS = require('aws-sdk');
const expect = require('chai').expect;

describe('dbService: When database returns a record', function() {
  afterEach(() => {
    sinon.restore();
  });
  it('Should have expected value', async function() {
    const mDynamo = { get: sinon.stub().returnsThis(), promise: sinon.stub().resolves({ Item: 'an item' }) };
    const mDocumentClient = sinon.stub(AWS.DynamoDB, 'DocumentClient').returns(mDynamo);
    const dbService = require('./dbService');
    const actual = await dbService.getItem();
    expect(actual.Item).to.be.equal('an item');
    sinon.assert.calledOnce(mDocumentClient);
    sinon.assert.calledWithExactly(mDynamo.get, {});
    sinon.assert.calledOnce(mDynamo.promise);
  });

  it('should return error', async () => {
    const mError = new Error('network');
    const mDynamo = { get: sinon.stub().returnsThis(), promise: sinon.stub().rejects(mError) };
    const mDocumentClient = sinon.stub(AWS.DynamoDB, 'DocumentClient').returns(mDynamo);
    const dbService = require('./dbService');
    const actual = await dbService.getItem();
    expect(actual.message).to.be.eql('network');
    sinon.assert.calledOnce(mDocumentClient);
    sinon.assert.calledWithExactly(mDynamo.get, {});
    sinon.assert.calledOnce(mDynamo.promise);
  });
});

单元测试结果:

  dbService: When database returns a record
    ✓ Should have expected value
    ✓ should return error


  2 passing (26ms)

--------------|---------|----------|---------|---------|-------------------
File          | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
--------------|---------|----------|---------|---------|-------------------
All files     |     100 |      100 |     100 |     100 |                   
 dbService.js |     100 |      100 |     100 |     100 |                   
--------------|---------|----------|---------|---------|-------------------