这些测试为什么通过?

Si-*_*i-N 7 javascript node.js sinon chai

我有这个功能:

let removePresentation = function(presentationName, callback) {
  let rimraf = require('rimraf');

  callback();
  callback();
  callback();

  if(!presentationName || !presentationName.trim()) {
    callback();
    return;
  }

  presentationName = presentationName.replace('.zip', '');

  rimraf('./presentations/' + presentationName, function(err) {
    if(err) {
      console.log(err);
    }
    callback();
  });
};

exports.removePresentation = removePresentation;
Run Code Online (Sandbox Code Playgroud)

我正在尝试使用以下方法进行测试:

var chai = require('chai'),
expect = require('chai').expect,
sinonChai = require('sinon-chai'),
sinon = require('sinon'),
mock = require('mock-require');

chai.use(sinonChai);

describe('removePresentation', function() {

  var sandbox;
  var callback;
  var rimrafSpy;

  beforeEach(function() {
    sandbox = sinon.sandbox.create();
    mock('../business/communications_business', {});

    rimrafSpy = sinon.spy();
    callback = sinon.spy();

    mock('rimraf', rimrafSpy);
  });

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

  it('should call rimraf if presentation name is valid', function(done) {
    let RoomStateBusiness = require('../business/roomstate_business');

    RoomStateBusiness.removePresentation('name.zip', callback);

    expect(rimrafSpy).to.have.been.calledWith('./presentations/name');
    expect(callback).to.have.been.called.once;
    done();
  });

  it('should not call rimraf if presentation name is null', function(done) {
    let RoomStateBusiness = require('../business/roomstate_business');

    RoomStateBusiness.removePresentation(null, callback);

    expect(rimrafSpy).not.to.have.been.called;
    expect(callback).to.have.been.called.once;
    done();
  });

  it('should not call rimraf if presentation name is whitespace', function(done) {
    let RoomStateBusiness = require('../business/roomstate_business');

    RoomStateBusiness.removePresentation('      ', callback);

    expect(rimrafSpy).not.to.have.been.called;
    expect(callback).to.have.been.called.once;
    done();
  });

  it('should not call rimraf if presentation name is empty string', function(done) {
    let RoomStateBusiness = require('../business/roomstate_business');

    RoomStateBusiness.removePresentation('', callback);

    expect(rimrafSpy).not.to.have.been.called;
    expect(callback).to.have.been.called.once;
    done();
  });

});
Run Code Online (Sandbox Code Playgroud)

即使我清楚地多次调用callback()(仅在进行测试时),expect(callback).to.have.been.called.once;始终会断言为true。我已经检查了Chai api,该api希望该调用完全是一次,尽管无论我调用callback()多少次,它都会一直传递。我究竟做错了什么?

Rob*_*ann 5

没有这样的主张expect(fn).to.have.been.called.once

根据sinon-chaidocs,只有:

  • expect(fn).to.have.been.called
  • expect(fn).to.have.been.calledOnce

问题

这是一个已知的问题,chai为什么只使用getter的断言是一件坏事。Chai允许您编写一段看起来像属性访问(即断言本身并不以函数调用结尾)的代码来断言...无论您要断言什么。这使用属性获取器执行必要的代码。

问题是,如果您输入错误或其他错误,则表达式将简单地求值为undefined(您正在访问的属性不存在)并且不会执行任何断言代码,从而导致测试通过(因为只有当引发异常)。

在您的情况下,有一个断言called,并且很可能返回一个对象。不幸的是,该对象没有assertion once,因此没有代码执行并且测试通过。

解决方案

您有2种选择:

  • 升级到具有Proxy支持的Chai 4和Node.js版本(不确定添加代理支持的位置,可能是Node.js 5或6)-chai通过代理对象代理所有属性访问来检查这些问题,以防止这些问题的发生使用有效的断言
  • 切勿使用getter进行断言,并始终以函数调用结束断言-这将确保如果您犯了一个错误,则测试将因臭名昭著的undefined is not a function错误而失败。

我认为,第二个选项是高度首选的,因为毫无疑问,测试用例的正确性。即使在受支持的平台上,仍然可以关闭Chai代理支持。