角度单元测试:模拟多个独立承诺

时间:2016-06-01 13:21:27

标签: javascript angularjs unit-testing jasmine promise

这是一个很长的问题,所以我将首先提出我所挣扎的问题:

如何解决在单元测试中使用不同参数运行的相同函数的独立承诺,并获得不同的值?

我在模拟执行多个http请求的环境时遇到了困难,这些请求彼此独立,但具有相同的服务对象。 它在实际应用中有效,但为单元测试(Jasmine,Karma)设置适当的模拟环境已经证明非常困难。

让我解释一下环境,以及我试图解释的内容:

首先,我有一个Angular Controller,它使用自定义服务对象生成一个http请求,并在测试中模拟这个。然后我创建了一个Controller,它使用相同的服务对象创建多个独立的http请求,并且考虑到我在另一个控制器上的成功,我尝试扩展我的单元测试来覆盖这个。

有关单一请求/承诺的控制器如何工作的背景知识

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

让我们首先使用单一请求控制器及其工作测试,以获得基础。

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 是一个自定义服务,它将http请求发送到设置的URL,并添加控制器传递的参数。

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();)

到目前为止,它有效。 我知道它不是最漂亮的代码,并且每次必须初始化然后解析模拟的承诺时,在每次测试中封装这些的方法都是优选的。我选择在这里展示这一切是为了示范目的。

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

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

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],这些值将被弹出以读取ABCD,以反映请求的实际顺序。

然而,问题:Resolve只发生一次,并且为所有四个结果属性创建了相同的响应:{value:" U"}。

在内部选择正确的列表,因为promise-chain使用与service-call(requestType)中使用的相同的参数值,但它们都只在第一个响应上接收数据。因此,结果是:

$ scope.resultA =" U&#34 ;; $ scope.resultB =" U"等等......而不是U,X,Y,Z。

因此,间谍功能运行了四次,我假设返回了四个承诺,每个呼叫一个。但截至目前,只有一个resolve()和一个q.digest()。 为了让事情顺利,我尝试了以下方法:

  • 四个q.defer()
  • 四个决议
  • 四种摘要
  • 返回一个包含四个不同对象的数组,对应于我在工作测试中的预期。 (愚蠢,我知道,它与预期的对象结构不同,但是当你尝试调整任何东西以获得令人惊讶的工作结果时,你不会做什么?)。

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

我已经尝试过谷歌这个问题,但我找到的只是对不同服务的多个承诺,多个链函数(.then()。then()...),或嵌套异步调用(新的promise对象( s)内链)。

我需要的是一个独立承诺的解决方案,通过使用不同参数运行相同的函数来创建。

所以,我将以我开辟的问题结束: 如何解决在单元测试中使用不同参数运行的相同函数的独立承诺,并获得不同的值?

1 个答案:

答案 0 :(得分:1)

Jasmine是所有行业的角度友好杰克。它通常适用于大多数前端测试用例。它缺乏间谍/模拟功能,而Sinon则提供更强大的功能。

这可能是Mocha / Sinon / Chai模块化捆绑在某些时候可能更受欢迎的原因,但其模块化的好处在于Sinon不依赖于捆绑。除了与Chai的紧密关系外,它还可以与Jasmine matchers一起使用。

使Sinon成为比Jasmine间谍更好的选择的事情是,它能够编程间谍期望(withArgs(...).called...)和存根响应(withArgs(...).returns(...))。蓝领嘲笑变得小菜一碟:

.returnValue

关于一次解决的承诺,这是预期的行为。根据经验,应该从模拟函数返回新的promise,而不是在Jasmine中使用.returns的现有对象(或Sinon中的var mockedPromiseValue; ... spy = spyOn(myHttpService, 'getServiceData') .and.callFake(() => $q.resolve(mockedPromiseValue)); ... mockedPromiseValue = ...; myHttpService.getServiceData().then((result) => { expect(result).toBe(...); }) // rinse and repeat $rootScope.$digest(); )。

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

diff -u <(git branch|sed 's/..//') <(git branch -r|sed 's/..origin\///')|tail -n +4|sed -n "s/^-//p" -