Nodejs的测试和测试驱动开发

测试是保证软件质量必不可少的一环。测试有很多形式:手动、自动、单元测试等等。这里我们只聊使用Mocha这个框架在Nodejs中实现单元测试。单元测试是测试等重要组成,这样的测试只对于一个方法,这样的一小段代码,实施有针对的测试。

这里会逐步深入的讲解单元测试。首先是最简单的单元测试,没有外部依赖,只有简单的输入。接着是实用Sino框架实现stub等有依赖的测试。最后讲解如何单元测试异步代码。

安装Mocha 和Chai

安装Mocha:

npm install mocha -g

Mocha和其他的javascript单元测试框架,如:jasmine和QUnit不同,他没有assertion库。但是,Mocha允许你实用你自己的。最流行的Assertion库有should.js、expect.js和Chai,当然Nodejs内置的也可以使用。这里我们用Chai。

首先创建一个package.json并安装Chai:

touch package.json
echo {} > package.json
npm install chai --save-dev

Chai包含三种assertion方式:should方式、expect方式和assert方式。个人喜欢expect式的,所以下面就使用这个方式了。

第一个Test

项目代码

第一个例子,我们用测试驱动开发(TDD)的方式创建一个CartSummary的构造函数,这个函数会用来计算购物车的商品总数。测试驱动开发就是在实现功能之前先写单元测试,这样来驱动你设计可以与测试相适应的代码。

测试驱动开发的步骤:

  1. 写一个测试,并且这个测试会失败。
  2. 写最少的代码来使整个测试可以通过。
  3. 重复。

来看代码:

// tests/part1/cart-summary-test.js
var chai = require('chai');
var expect = chai.expect; // we are using the "expect" style of Chai
var CartSummary = require('./../../src/part1/cart-summary');

describe('CartSummary', function() {
  it('getSubtotal() should return 0 if no items are passed in', function() {
    var cartSummary = new CartSummary([]);
    expect(cartSummary.getSubtotal()).to.equal(0);
  });
});

describe方法是用来创建一组测试的,并且可以给这一组测试一个描述。一个测试就用一个it方法。it方法的第一个参数是一个描述。第二个参数是一个包含一个或者多个assertion的方法。

运行测试只需要在项目的根目录运行命令行:mocha tests --recursive --watchrecursive指明会找到根目录下的子目录的测试代码并运行。watch则表示Mocha会监视源代码和测试代码的更改,每次更改之后重新测试。

我们测试不过,因为还没有完成功能代码。添加代码:

// src/part1/cart-summary.js
function CartSummary() {}

CartSummary.prototype.getSubtotal = function() {
  return 0;
};

module.exports = CartSummary;

测试就可以通过了:

下一个测试:

it('getSubtotal() should return the sum of the price * quantity for all items', function() {
  var cartSummary = new CartSummary([{
    id: 1,
    quantity: 4,
    price: 50
  }, {
    id: 2,
    quantity: 2,
    price: 30
  }, {
    id: 3,
    quantity: 1,
    price: 40
  }]);

  expect(cartSummary.getSubtotal()).to.equal(300);
});

这个测试时失败的。。。

下面就来修改代码,让测试通过:

// src/part1/cart-summary.js
function CartSummary(items) {
  this._items = items;
}

CartSummary.prototype.getSubtotal = function() {
  if (this._items.length) {
    return this._items.reduce(function(subtotal, item) {
      return subtotal += (item.quantity * item.price);
    }, 0);
  }

  return 0;
};

Stub和Sinon

假设我们现在需要给CartSummary添加getTax方法。最终的使用看起来是这样的:

var cartSummary = new CartSummary([ /* ... */ ]);
cartSummary.getTax('NY', function() {
  // executed when the tax API request has finished
});

getTax方法会使用量外的一个tax模块,包含一个calculate的方法。虽然我们还没有实现tax模块,但是我们还是可以完成getTax的测试。该怎么做呢?

首先,安装Sinon:

npm install --save-dev sinon

安装Sinon之后,我们就可以给出tax.calculate的定义了:


// src/part1/tax.js
module.exports = {
  calculate: function(subtotal, state, done) {
    // implemented later or in parallel by our coworker
  }
};

创建完成tax.calculate之后就可以使用Sinon的魔法了。用Sinon给出一个tax.calculate的零时实现。这个零时的实现就是Stub(也叫做桩)。代码:

// tests/part1/cart-summary-test.js
// ...
var sinon = require('sinon');
var tax = require('./../../src/part1/tax');

describe('getTax()', function() {
  beforeEach(function() {
    sinon.stub(tax, 'calculate', function(subtotal, state, done) {
      setTimeout(function() {
        done({
          amount: 30
        });
      }, 0);
    });
  });

  afterEach(function() {
    tax.calculate.restore();
  });

  it('get Tax() should execute the callback function with the tax amount', function(done) {
    var cartSummary = new CartSummary([{
      id: 1,
      quantity: 4,
      price: 50
    }, {
      id: 2,
      quantity: 2,
      price: 30
    }, {
      id: 3,
      quantity: 1,
      price: 40
    }]);

    cartSummary.getTax('NY', function(taxAmount) {
      expect(taxAmount).to.equal(30);
      done();
    });
  });
});

上面已经使用Sinon创建stub方法了。这里再细讲一下。使用sinon.stub方法创建Stub:

var stub = sinon.stub(object,'method', func);

object添加一个名称为method(第二个参数)的方法,方法体的实现在第三个参数中给出。

上例中使用的方法体:

function(subtotal, state, done) {
  setTimeout(function() {
    done({
      amount: 30
    });
  }, 0);
}

setTimeout方法是用来模拟真实环境的,在实际使用的时候肯定会有一个异步的网络请求来请求tax服务。方法体的替换在beforeEach里,这些代码会在测试开始之前执行。在所有测试完成之后调用afterEach,并把tax.calculate恢复到原来的模样。

上面的例子也展示了如何测试异步代码。在it方法中指明一个参数(上例使用的是done)。Mocha会传入一个方法,并等待异步代码返回再结束测试。当然,这个等待是由超时时间的,一般是2000毫秒。如果异步代码的测试,没有按照上面的方法写的话,那么所有的测试都会通过。

Sinon的"间谍"

Sinon的间谍(spy)是用来完成另外一种替身测试的(test double),它可以用来记录方法调用。包括方法的调用次数、调用的时候的参数是什么样的以及是否抛出异常。下面就是更新后的测试:

it('getTax() should execute the callback function with the tax amount', function(done) {
  var cartSummary = new CartSummary([
    {
      id: 1,
      quantity: 4,
      price: 50
    },
    {
      id: 2,
      quantity: 2,
      price: 30
    },
    {
      id: 3,
      quantity: 1,
      price: 40
    }
  ]);

  cartSummary.getTax('NY', function(taxAmount) {
    expect(taxAmount).to.equal(30);
    expect(tax.calculate.getCall(0).args[0]).to.equal(300);
    expect(tax.calculate.getCall(0).args[1]).to.equal('NY');
    done();
  });
});

在测试中添加了两个expect。getCall用来获取tax.calculate的第一次调用的第一个参数值,第二个getCall用来获取tax.calculate的第一次调用的第二个参数。主要可以用来检测被测试方法的参数是否正确。

总结

在本文中探讨了如何在Node中使用Mocha以及Chai和Sinon实现单元测试。希望各位喜欢。

原文地址:https://www.codementor.io/nodejs/tutorial/unit-testing-nodejs-tdd-mocha-sinon

推荐阅读更多精彩内容