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