Как я могу заглушить обещание, чтобы мой тест мог выполняться синхронно?

Я пытаюсь выполнить модульное тестирование модуля, отключив одну из его зависимостей, в данном случае UserManager

Упрощенная версия модуля выглядит следующим образом:

// CodeHandler
module.exports = function(UserManager) {
  return {
    oAuthCallback: function(req, res) {
      var incomingCode = req.query.code;
      var clientKey = req.query.key;
      UserManager.saveCode(clientKey, incomingCode)
        .then(function(){
          res.redirect('https://test.tes');
        }).catch(function(err){
          res.redirect('back');
        }
      );
    }
  };
};

Я заглушаю функцию saveCode UserManager, которая возвращает Promise, так что она возвращает разрешенное обещание, но когда я assert вызывал res.redirect, увы, во время утверждения res.redirect еще не вызывался.

Упрощенная версия модульного теста:

// test
describe('CodeHandler', function() {
  var req = {
    query: {
      code: 'test-code',
      key: 'test-state'
    }
  };

  var res = {
    redirect: function() {}
  };

  var expectedUrl = 'https://test.tes';
  var ch;

  beforeEach(function() {
    sinon.stub(UserManager, 'saveCode').returns(
      new RSVP.Promise(function(resolve, reject){
        resolve();
      })
    );

    sinon.stub(res, 'redirect');

    ch = CodeHandler(UserManager);
  });

  afterEach(function() {
    UserManager.saveCode.restore();
    res.redirect.restore();
  });

  it('redirects to the expected URL', function(){
    ch.oAuthCallback(req, res);
    assert(res.redirect.calledWith(expectedUrl));
  })
});

Как я могу правильно заглушить промис, чтобы тестируемый метод работал синхронно?


person Dave Sag    schedule 29.09.2015    source источник
comment
Какую среду тестирования вы используете? разве у него нет поддержки асинхронности?   -  person Amit    schedule 29.09.2015
comment
Использование mocha и да, у него есть асинхронная поддержка, но в данном случае это мне не поможет. Тестируется не обещание, а код, реагирующий на результат обещания.   -  person Dave Sag    schedule 29.09.2015
comment
res не определено в вашем тестовом коде. если бы это была реальная функция, вы могли бы вызвать done внутри нее (которую вы получаете в качестве входных данных для своей тестовой функции)   -  person Amit    schedule 29.09.2015
comment
извините, да, вы правы, я забыл определить res в этом отрывке из моего фактического теста. Я исправлю это для полноты картины.   -  person Dave Sag    schedule 30.09.2015


Ответы (2)


Я разработал решение, используя sinon-stub-promise.

describe('CodeHandler', function() {
  var req = {
    query: {
      code: 'test-code',
      key: 'test-state'
    }
  };
  var ch;
  var promise;

  var res = {
    redirect: function() {}
  };

  beforeEach(function() {
    promise = sinon.stub(UserManager, 'saveCode').returnsPromise();
    ch = CodeHandler(UserManager);
    sinon.stub(res, 'redirect');
  });

  afterEach(function() {
    UserManager.saveCode.restore();
    res.redirect.restore();
  });

  describe('can save code', function() {
    var expectedUrl = 'https://test.tes';

    beforeEach(function() {
        promise.resolves();
    });

    it('redirects to the expected URL', function(){
      ch.oAuthCallback(req, res);
      assert(res.redirect.calledWith(expectedUrl));
    });
  });

  describe('can not save code', function() {
    var expectedUrl = 'back';

    beforeEach(function() {
        promise.rejects();
    });

    it('redirects to the expected URL', function(){
      ch.oAuthCallback(req, res);
      assert(res.redirect.calledWith(expectedUrl));
    })
  })
});

Это работает отлично.

person Dave Sag    schedule 29.09.2015

Что ж, проще всего было бы не заглушить его для синхронного запуска вообще, поскольку это может изменить порядок выполнения и использовать встроенную в Mocha поддержку промисов (или jasmine-as-promised при использовании jasmine).

Причина в том, что могут быть такие случаи, как:

somePromise.then(function(){
    doB();
});
doA();

Если вы вызываете обещания для синхронного разрешения, порядок выполнения - и, следовательно, вывод программы изменяется, что делает тест бесполезным.

Напротив, вы можете использовать синтаксис теста:

describe("the test", () => { // use arrow functions, node has them and they're short
    it("does something", () => {
        return methodThatReturnsPromise().then(x => {
           // assert things about x, throws will be rejections here
           // which will cause a test failure, so can use `assert`
        });
    });
});

Вы можете использовать еще более легкий синтаксис стрелки для отдельных строк, что сделает тест еще менее подробным:

describe("the test", () => { // use arrow functions, node has them and they're short
  it("does something", () =>
      methodThatReturnsPromise().then(x => {
         // assert things about x, throws will be rejections here
         // which will cause a test failure, so can use `assert`
      });
  );
});

Насколько я знаю, в RSVP вы не можете установить планировщик, поэтому в любом случае невозможно синхронно тестировать вещи, другие библиотеки, такие как bluebird, позволяют вам делать это на свой страх и риск, но даже в библиотеках, которые позволяют вам это делать, это, вероятно, не лучшая идея.

person Benjamin Gruenbaum    schedule 29.09.2015
comment
Тестируется не само обещание, а код, который отвечает на тестируемое обещание. - person Dave Sag; 29.09.2015
comment
@DaveSag, тогда этот код также должен возвращать обещание (поместите return перед UserManager.saveCode`), чтобы вы могли протестировать его, а затем использовать асинхронный синтаксис. - person Benjamin Gruenbaum; 29.09.2015