mercredi 1 juin 2016

Angular Unit tests: Mocking multiple independent promises

This is a long one, so I will begin by asking the question I struggle with:

How do I resolve independent promises for the same function that has been run with different parameters in unit testing, and get different values?

I have difficulties with mocking an environment where multiple http-requests are executed, independent of each other, but with the same service-object. It works in real application, but setting up a proper mocking environment for unit-testing (Jasmine, Karma) has proven quite difficult.

Let me explain the environment, and what I have tried to to:

First off, I have an Angular Controller that makes a single http-request with a custom service object, and mocking this in the tests works. Then I have made a Controller that makes multiple independent http-requests with the same service object, and I have attempted at expanding my unit testing to cover this one, given my success with the other controller.

Background on how it works in controller with single request/promise:

If you don't want to go through all this, you can jump straight to The real problem: Testing multiple independent requests and promises. You probably should.

Let us first go with the single-request controller and its working test, to have a foundation.

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
}

As you probably figure, myHttpService is a custom service that sends a http-request to a set URL, and adds in the parameters passed on by the controller.

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");
        }); 
    });
});

This is a light-weight pseudo copy of a real life implementation I'm working with. Suffice to say, I have, through trying and failing, discovered that for each and every call on myHttpService.getServiceData (usually by directly calling $scope.executeSingleRequest, or indirectly through other methods), the following has to be done:

  • myHttpServiceMock must be initialized anew (myHttpServiceMock= q.defer();),
  • initialize spy to return mocked promise (spy.and.returnValue(myHttpServiceMock.promise);)
  • Execute the call to the service
  • Resolve the promise (myHttpServiceMock.resolve({data : "This is a fake response"});)
  • Call digest (q.defer();)

So far, it works. I know it's not the most beautiful code, and for each time the mocked promise has to be initialized and then resolved, a method encapsulating these would be preferable in each test. I've chosen to show it all here for demonstrative purpose.

The real problem: Testing multiple independent requests and promises:

Now, let us say the controller does multiple independent requests to the service, with different parameters. This is the case in a similar controller in my real life application:

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
}

Now, I realize that "assignToResultBasedOnType" may not be the best way to handle the assignment to the correct property, but that is what we have today.

Usually, the four different result-properties receive the same type of object, but with different content, in the real life application. Now, I want to simulate this behavior in my test.

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"}}
    };
});

This is what happens in the test (discovered during debug): The spy function runs four times, and pushes in the values to the array lastRequestTypeParameter, which will be [D, C, B, A], which values are supposed will be popped to read A-B-C-D, to reflect the real order of the requests.

However, here comes the problem: Resolve happens only once, and the same response is created for all four result-properties: {value:"U"}.

The correct list is selected internally, because the promise-chain uses the same parameter values as was used in the service-call (requestType), but they all receive data only on the first response. Thus, the result is:

$scope.resultA = "U"; $scope.resultB = "U", and so on.... instead of U, X, Y, Z.

So, the spy function runs four times, and I had assumed that four promises were returned, one for each call. But as of now, there is only one resolve() and one q.digest(). I have tried the following, to make things work:

  • Four q.defer()
  • Four resolves
  • Four digests
  • Return an array with four different objects, corresponding to what I would expect in working test. (Silly, I know, it differs from the expected object structure, but what don't you do when you try to tweak anything to get a surprisingly working result?).

None of these work. In fact, the first resolve causes the same result to all four properties, so adding more resolves and digests will make little difference.

I have tried to Google this issue, but all I find are either multiple promises for different services, multiple chain-functions (.then().then()...), or nested asynchronous calls (new promise object(s) inside chain).

What I need is a solution for independent promises, created by running the same function with different parameters.

So, I will end with the question I opened up with: How do I resolve independent promises for the same function that has been run with different parameters in unit testing, and get different values?

Aucun commentaire:

Enregistrer un commentaire