javascript - Angular单元测试:模拟多个独立的Promise

标签 javascript angularjs unit-testing jasmine promise

这是一个漫长的过程,因此我将首先问一个我遇到的问题:

如何为在单元测试中使用不同参数运行的同一功能解析独立的承诺,并获得不同的值?

在模拟执行多个http请求且彼此独立但具有相同服务对象的环境时,我遇到了困难。
它可以在实际应用程序中工作,但是事实证明,为单元测试(Jasmine,Karma)建立适当的模拟环境非常困难。

让我解释一下环境以及我试图做到的:

首先,我有一个Angular Controller,它使用自定义服务对象进行单个http请求,并在测试中对此进行了模拟。然后,我制作了一个Controller,可以使用同一个服务对象进行多个独立的http请求,并且考虑到我在另一个Controller上的成功,我尝试扩展我的单元测试以涵盖该测试。

关于单个请求/承诺在控制器中如何工作的背景:

如果您不想经历所有这些,则可以直接跳至真正的问题:测试多个独立的请求和承诺。你可能应该。

让我们首先来看一下单请求控制器及其工作测试,以奠定基础。

SingleRequestController

function OpenDataController($scope, myHttpService) {

    $scope.parameterData = {requestString : "A"};
    $scope.executeSingleRequest = function() {
        myHttpService.getServiceData($scope.parameterData)
            .then(function (response) {
                $scope.result = response.data;
            });
    }

    // Assume other methods, that calls on $scope.executeSingleRequest, $scope.parameterData may also change
}


您可能会想到,myHttpService是一个自定义服务,它向设置的URL发送http请求,并添加控制器传递的参数。

SingleRequestControllerTest

describe('SingleRequestController', function() {

    var scope, controller, myHttpServiceMock, q, spy;

    beforeEach(module('OppgaveregisteretWebApp'));

    beforeEach(inject(function ($controller, $q, $rootScope, myHttpService) {

        rootScope = $rootScope;
        scope = rootScope.$new();
        q = $q;

        spy = spyOn(myHttpService, 'getServiceData');

        // Following are uncommented if request is executed at intialization
        //myHttpServiceMock= q.defer();
        //spy.and.returnValue(myHttpServiceMock.promise);

        controller = $controller('OpenDataController', {
            $scope: scope,
            httpService: httpService
        });

        // Following are uncommented if request is executed at intialization
        //myHttpServiceMock.resolve({data : "This is a fake response"});
        //scope.$digest();

    }));

    describe('executeSingleRequest()', function () {

        it('should update scope.result after running the service and receive response', function () {

            // Setup example
            scope.parameterdata = {requestString : "A", requestInteger : 64};

            // Prepare mocked promises.
            myHttpServiceMock= q.defer();
            spy.and.returnValue(myHttpServiceMock.promise);

            // Execute method
            scope.executeSingleRequest();

            // Resolve mocked promises
            myHttpServiceMock.resolve({data : "This is a fake response"});
            scope.$digest();

            // Check values
            expect(scope.result).toBe("This is a fake response");
        }); 
    });
});


这是我正在使用的现实实现的轻量级伪副本。可以说,通过尝试和失败,我发现对于myHttpService.getServiceData的每个调用(通常是直接调用$ scope.executeSingleRequest或通过其他方法间接调用),都必须执行以下操作:


必须重新初始化myHttpServiceMock(myHttpServiceMock = q.defer();),
初始化间谍以返回模拟承诺(spy.and.returnValue(myHttpServiceMock.promise);)
执行对服务的呼叫
兑现承诺(myHttpServiceMock.resolve({data:“这是假响应”});)
呼叫摘要(q.defer();)


到目前为止,它仍然有效。
我知道这不是最漂亮的代码,并且每次必须对模拟的Promise进行初始化然后再进行解析时,在每次测试中最好使用封装这些方法的方法。我选择在此处显示所有这些内容以进行演示。

真正的问题:测试多个独立的请求和承诺:

现在,让我们说控制器使用不同的参数对服务执行多个独立请求。在我的实际应用程序中的类似控制器中就是这种情况:

MultipleRequestsController

function OpenDataController($scope, myHttpService) {

    $scope.resultA = "";
    $scope.resultB = "";
    $scope.resultC = "";
    $scope.resultD = "";

    $scope.executeRequest = function(parameterData) {
        myHttpService.getServiceData(parameterData)
            .then(function (response) {
                assignToResultBasedOnType(response, parameterData.requestType);
            });
    }

    $scope.executeMultipleRequestsWithStaticParameters = function(){
        $scope.executeRequest({requestType: "A"});
        $scope.executeRequest({requestType: "B"});
        $scope.executeRequest({requestType: "C"});
        $scope.executeRequest({requestType: "D"});
    };

    function assignToResultBasedOnType(response, type){
        // Assign to response.data to 
        // $scope.resultA, $scope.resultB, 
        // $scope.resultC, or $scope.resultD, 
        // based upon value from type

        // response.data and type should differ,
        // based upon parameter "requestType" in each request
        ...........
    };

    // Assume other methods that may call upon $scope.executeMultipleRequestsWithStaticParameters or $scope.executeRequest
}


现在,我意识到“ assignToResultBasedOnType”可能不是处理分配给正确属性的最佳方法,但这就是我们今天所拥有的。

通常,在实际应用程序中,四个不同的结果属性接收相同类型的对象,但内容不同。
现在,我想在测试中模拟这种行为。

MultipleRequestControllerTest

describe('MultipleRequestsController', function() {

    var scope, controller, myHttpServiceMock, q, spy;

    var lastRequestTypeParameter = [];

    beforeEach(module('OppgaveregisteretWebApp'));

    beforeEach(inject(function ($controller, $q, $rootScope, myHttpService) {

        rootScope = $rootScope;
        scope = rootScope.$new();
        q = $q;

        spy = spyOn(myHttpService, 'getServiceData');

        controller = $controller('OpenDataController', {
            $scope: scope,
            httpService: httpService
        });

    }));

    describe('executeMultipleRequestsWithStaticParameters ()', function () {

        it('should update scope.result after running the service and receive response', function () {

            // Prepare mocked promises.
            myHttpServiceMock= q.defer();
            spy.and.callFake(function (myParam) {
                lastRequestTypeParameter.unshift(myParam.type);
                return skjemaHttpServiceJsonMock.promise;

            // Execute method
            scope.executeMultipleRequestsWithStaticParameters();

            // Resolve mocked promises
            myHttpServiceMock.resolve(createFakeResponseBasedOnParameter(lastRequestTypeParameter.pop()));
            scope.$digest();

            // Check values
            expect(scope.resultA).toBe("U");
            expect(scope.resultB).toBe("X");
            expect(scope.resultC).toBe("Y");
            expect(scope.resultD).toBe("Z");
        }); 
    });

    function createFakeResponseBasedOnParameter(requestType){
        if (requestType==="A"){return {value:"U"}}
        if (requestType==="B"){return {value:"X"}}
        if (requestType==="C"){return {value:"Y"}}
        if (requestType==="D"){return {value:"Z"}}
    };
});


这是测试中发生的事情(在调试过程中发现):
间谍函数运行四次,然后将值推入数组lastRequestTypeParameter,该数组将为[D,C,B,A],应该弹出该值以读取A-B-C-D,以反映请求的真实顺序。

但是,问题来了:解析仅发生一次,并且为所有四个结果属性创建相同的响应:{value:“ U”}。

在内部选择正确的列表,因为Promise链使用的参数值与服务调用(requestType)中使用的参数值相同,但是它们都仅在第一个响应上接收数据。因此,结果是:

$ scope.resultA =“ U”; $ scope.resultB =“ U”,依此类推....代替U,X,Y,Z。

因此,间谍功能运行了四次,我假设已经返回了四个承诺,每个调用一个。但是到目前为止,只有一个resolve()和一个q.digest()。
我尝试了以下方法,以使工作正常:


四个q.defer()
四个解决方案
四个摘要
返回一个包含四个不同对象的数组,与我在工作测试中所期望的相对应。 (愚蠢,我知道,它与预期的对象结构不同,但是当您尝试进行任何调整以获得令人惊讶的工作结果时,您不做什么?)。


这些都不起作用。实际上,第一个解析会对所有四个属性产生相同的结果,因此添加更多的解析和摘要将没有什么区别。

我已经尝试过Google这个问题,但我发现的只是针对不同服务的多个Promise,多个链函数(.then()。then()...)或嵌套的异步调用(内部的新Promise对象)链)。

我需要的是独立承诺的解决方案,该解决方案是通过运行具有不同参数的相同功能而创建的。

因此,我将以我提出的问题结尾:
如何为在单元测试中使用不同参数运行的同一功能解析独立的承诺,并获得不同的值?

最佳答案

茉莉是所有行业中对Angular友好的杰克。它通常适用于大多数前端测试用例。它缺少间谍/模拟功能,而Sinon提供了更多功能。

这可能是为什么在某些时候可能首选Mocha / Sinon / Chai模块化捆绑包的原因,但是其模块化的好处是,Sinon不受捆绑捆绑。除了与Chai的紧密关系外,它还可以与Jasmine matchers一起使用。

使Sinon成为比Jasmine间谍更好的选择的原因是,它可以对间谍期望值(withArgs(...).called...)和响应的响应(withArgs(...).returns(...))进行编程。蓝领嘲笑可谓小菜一碟:

var sandbox;
var spy;

// beforeEach
sandbox = sinon.sandbox.create();
// similar to Jasmine spy without callThrough
spy = sandbox.stub(myHttpService, 'getServiceData');
...

// it
spy.withArgs('A').returns({value:"U"});
spy.withArgs('B').returns({value:"X"});
...

// afterEach
sandbox.restore(); // the thing that Jasmine does automatically for its spies


关于曾经解决的承诺,这是预期的行为。根据经验,应该从模拟函数中返回新的Promise,决不能在Jasmine中(或Sinon中的.returnValue)中存在带有.returns的现有对象。

应使用回调函数在每次调用时返回新的Promise。如果应使用预定义的值来解决承诺,则可能有几种模式可以实现此目的,最明显的是使用变量

var mockedPromiseValue;

...
spy = spyOn(myHttpService, 'getServiceData')
  .and.callFake(() => $q.resolve(mockedPromiseValue));
...

mockedPromiseValue = ...;
myHttpService.getServiceData().then((result) => {
  expect(result).toBe(...);
})

// rinse and repeat

$rootScope.$digest();

关于javascript - Angular单元测试:模拟多个独立的Promise,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37569966/

相关文章:

C# wpf UnitTesting View 模型

Perl 测试 - 测试的公共(public)父级

angularjs - 为什么我不能在我的 Controller 中访问这个 ng-model 变量?

javascript 删除字符串上的页眉和页脚

c# - 如何在 .NET 的单个单元测试中修改语言环境小数点分隔符?

javascript - 按错误对象集合中值为 true 的属性过滤

javascript - 将传单中的自定义图标添加到 geojson 文件

javascript - 在 tinyMCE 工具栏上向右浮动按钮

javascript - 故事书 4 和 webpack.config?

data-binding - angular.js(或 knockout.js)与其他 UI 库的集成?