Testing an asynchronous function with jasmine
We are experiencing unexpected behavior while testing asynchronous code with Jasmine. As far as we know, when you use a function done
, expectations are not called until the execution is done. But this doesn't happen because the second expectation fails, so the assignment $ctrl.todos
never happened
Test not working
it('initializes the data when $onIinit', (done) => {
const expected = 'some result';
const response = Promise.resolve(expected);
spyOn(myService, 'getAll').and.returnValue(response);
// This method calls myService.getAll
$ctrl.$onInit();
expect(myService.getAll).toHaveBeenCalled();
expect($ctrl.todos).toEqual(false);
response.then(done);
});
Output: Expected undefined equals false
On the other hand, this works:
it('initializes the data when $onIinit', (done) => {
const expected = 'some result';
const response = Promise.resolve(expected);
spyOn(myService, 'getAll').and.returnValue(response);
// This method calls myService.getAll
$ctrl.$onInit();
expect(myService.getAll).toHaveBeenCalled();
response
.then(() => expect($ctrl.todos).toBe(expected))
.then(done);
});
Output: test pass
Controller method:
$ctrl.$onInit = () => {
myService.getAll().then((data) => {
$ctrl.todos = data;
});
};
source to share
I found a solution. I don't need another one then
. The specifications with done
will not be completed until this is called. Moreover, it should always be placed after expectations.
Working code:
it('initializes the data when $onIinit', (done) => {
const expected = 'some result';
const response = Promise.resolve(expected);
spyOn(myService, 'getAll').and.returnValue(response);
$ctrl.$onInit();
expect(myService.getAll).toHaveBeenCalled();
response
.then(() => {
expect($ctrl.todos).toBe(expected);
done();
});
});
source to share
Your aproach seems to be correct and probably a call done
inside afterEach
will make it work.
afterEach(function(done) {
done();
}, 1000);
But I would recommend using $ httpBackend , a fake HTTP backend support suitable for unit testing applications that use the $ http service . We use it anyway angularjs
, right? So why not take advantage of the benefits?
With $ httpBackend, we can execute requests and then respond with mock data without sending the request to the real server. Here's an illustrative example
it('initializes the data when $onIinit', () => {
const mockData = { data: { expected: 'some result' } };
spyOn(myService, 'getAll').and.callThrough();
$httpBackend.expect('POST', '/my-service/url').respond(200, mockData);
// This method calls myService.getAll
$ctrl.$onInit();
//flush pending request
$httpBackend.flush();
expect(myService.getAll).toHaveBeenCalled();
expect($ctrl.todos).toBeDefined();
});
In some clarification $httpBackend.expect('POST', '/my-service/url')
here, note that 'POST'
you need to map method
that is used by your service in myService.getAll
, and '/my-service/url'
- this url
is also used by your service in myService.getAll
.
Required to be called $httpBackend.flush();
, it will output all pending requests.
You will need to enter $httpBackend
in your tests, an easy way would be
describe('$httpBackend service in module ngMock', () => {
let $httpBackend;
beforeEach(inject(['$httpBackend', (_$httpBackend) => {
$httpBackend = _$httpBackend;
}]));
it('$httpBackend is defined', () => {
// here we can use $httpBackend
expect($httpBackend).toBeDefined();
});
});
Also note that it $httpBackend
is part of the ngMock module .
More info on testing angularjs code here
Hope it helps
source to share
Promise
located outside the Angular world - you need to wait for the result to be available in the next event queue. tick - dirty (almost dirty) hack usagesetTimeout
angular.module('test', [])
.controller('underTest', function($scope, myService) {
$scope.$onInit = function() {
myService.getAll().then(function(data) {
$scope.todos = data
})
}
})
describe('Controller: somethingChanged', function() {
var scope, myService
beforeEach(function() {
module('test')
})
beforeEach(function() {
module(function($provide) {
$provide.value('myService', {
getAll: function() {}
})
})
})
beforeEach(inject(function($controller, _$rootScope_, _myService_) {
myService = _myService_
scope = _$rootScope_.$new()
$controller('underTest', {
$scope: scope
})
}))
it('initializes the data when $onIinit', function(done) {
const expected = 'some result'
const response = Promise.resolve(expected)
spyOn(myService, 'getAll').and.returnValue(response)
scope.$onInit()
expect(myService.getAll).toHaveBeenCalled();
setTimeout(function() {
expect(scope.todos).toEqual(expected)
done()
})
});
});
<script src="https://cdn.jsdelivr.net/jasmine/2.6.1/jasmine.js"></script>
<script src="https://cdn.jsdelivr.net/jasmine/2.6.1/jasmine-html.js"></script>
<script src="https://cdn.jsdelivr.net/jasmine/2.6.1/boot.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/jasmine/2.6.1/jasmine.css" />
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular-mocks.js"></script>
source to share