Angular Unit Test Failing Expected spy - javascript

I have below controller to get the books list and single books detail. It's working as expected but the unit test is not working as expected.
books.controller.js
var myApp = angular.module('myApp');
function BooksController($log, $routeParams, BooksService) {
// we declare as usual, just using the `this` Object instead of `$scope`
const vm = this;
const routeParamId = $routeParams.id;
if (routeParamId) {
BooksService.getBook(routeParamId)
.then(function (data) {
$log.info('==> successfully fetched data for book id:', routeParamId);
vm.book = data;
})
.catch(function (err) {
vm.errorMessage = 'OOPS! Book detail not found';
$log.error('GET BOOK: SOMETHING GOES WRONG', err)
});
}
BooksService.getBooks()
.then(function (data) {
$log.info('==> successfully fetched data');
vm.books = data;
})
.catch(function (err) {
vm.errorMessage = 'OOPS! No books found!';
$log.error('GET BOOK: SOMETHING GOES WRONG', err)
});
}
BooksController.$inject = ['$log', '$routeParams', 'BooksService'];
myApp.controller('BooksController', BooksController);
Spec for above controller in which I want to test the getBook(id) service but somehow I am not able to pass the id of book.
describe('Get All Books List: getBooks() =>', () => {
const errMsg = 'OOPS! No books found!';
beforeEach(() => {
// injecting rootscope and controller
inject(function (_$rootScope_, _$controller_, _$q_, BooksService) {
$scope = _$rootScope_.$new();
$service = BooksService;
$q = _$q_;
deferred = _$q_.defer();
// Use a Jasmine Spy to return the deferred promise
spyOn($service, 'getBooks').and.returnValue(deferred.promise);
// The injector unwraps the underscores (_) from around the parameter names when matching
$vm = _$controller_('BooksController', {$scope: $scope, $service: BooksService});
});
});
it('should defined getBooks $http methods in booksService', () => {
expect(typeof $service.getBooks).toEqual('function');
});
it('should able to fetch data from getBooks service', () => {
// Setup the data we wish to return for the .then function in the controller
deferred.resolve([{ id: 1 }, { id: 2 }]);
// We have to call apply for this to work
$scope.$apply();
// Since we called apply, now we can perform our assertions
expect($vm.books).not.toBe(undefined);
expect($vm.errorMessage).toBe(undefined);
});
it('should print error message if data not fetched', () => {
// Setup the data we wish to return for the .then function in the controller
deferred.reject(errMsg);
// We have to call apply for this to work
$scope.$apply();
// Since we called apply, now we can perform our assertions
expect($vm.errorMessage).toBe(errMsg);
});
});
describe('Get Single Book Detail: getBook() =>', () => {
const errMsg = 'OOPS! Book detail not found';
const routeParamId = '59663140b6e5fe676330836c';
beforeEach(() => {
// injecting rootscope and controller
inject(function (_$rootScope_, _$controller_, _$q_, BooksService) {
$scope = _$rootScope_.$new();
$scope.id = routeParamId;
$service = BooksService;
$q = _$q_;
var deferredSuccess = $q.defer();
// Use a Jasmine Spy to return the deferred promise
spyOn($service, 'getBook').and.returnValue(deferredSuccess.promise);
// The injector unwraps the underscores (_) from around the parameter names when matching
$vm = _$controller_('BooksController', {$scope: $scope, $service: BooksService});
});
});
it('should defined getBook $http methods in booksService', () => {
expect(typeof $service.getBook).toEqual('function');
});
it('should print error message', () => {
// Setup the data we wish to return for the .then function in the controller
deferred.reject(errMsg);
// We have to call apply for this to work
$scope.$apply();
// expect($service.getBook(123)).toHaveBeenCalled();
// expect($service.getBook(123)).toHaveBeenCalledWith(routeParamId);
// Since we called apply, now we can perform our assertions
expect($vm.errorMessage).toBe(errMsg);
});
});
"Get Single Book Detail: getBook()" this suit is not working. Please help me, how to short out this kind of situation.
Error I am getting is below
Chrome 59.0.3071 (Mac OS X 10.12.5) Books Controller Get Single Book Detail: getBook() => should print error message FAILED
Expected 'OOPS! No books found!' to be 'OOPS! Book detail not found'.
Chrome 59.0.3071 (Mac OS X 10.12.5) Books Controller Get Single Book Detail: getBook() => should print error message FAILED
Expected 'OOPS! No books found!' to be 'OOPS! Book detail not found'.
at Object.it (test/client/controllers/books.controller.spec.js:108:38)
Chrome 59.0.3071 (Mac OS X 10.12.5): Executed 7 of 7 (1 FAILED) (0 secs / 0.068 secs)
.
Chrome 59.0.3071 (Mac OS X 10.12.5): Executed 7 of 7 (1 FAILED) (0.005 secs / 0.068 secs)

you need to mock $rootScope. with provide.
The value of id is not getting avaibale in controller which is undefined.
So, non-id condition getting executing.
$scope = _$rootScope_.$new();
$scope.id = routeParamId;
module(function ($provide) {
$provide.value('$rootScope', scope); //mock rootscope with id
});

Real router should never be used in unit tests, with ngRoute module preferably be excluded from tested modules.
$scope.id = routeParamId is assigned before controller instantiation, but it isn't used at all. Instead, it should be done with mocked $routeParams.
There's no $service service. It's called BooksService. Thus getBooks isn't a spy. It's preferable to mock the service completely, not only a single method.
mockedBooksService = jasmine.createSpyObj('BooksService', ['getBooks']);
var mockedData1 = {};
var mockedData2 = {};
mockedBooksService.getBooks.and.returnValues(
$q.resolve(mockedData1),
$q.resolve(mockedData2),
);
$vm = $controller('BooksController', {
$scope: $scope,
BooksService: mockedBooksService,
$routeParams: { id: '59663140b6e5fe676330836c' }
});
expect(mockedBooksService.getBooks).toHaveBeenCalledTimes(2);
expect(mockedBooksService.getBooks.calls.allArgs()).toEqual([
['59663140b6e5fe676330836c'], []
]);
$rootScope.$digest();
expect($vm.book).toBe(mockedData2);
// then another test for falsy $routeParams.id
The test reveals the problem in controller code. Since tested code is called on controller construction, $controller should be called every time in it. A good way to avoid this is to put initialization code into $onInit method that could be tested separately.

EDIT (removed original, 2am answer)
Are you using strict mode? There appear to be a few scoping issues going on:
On line 9 (in the "Get All Books List" spec), deferred is not declared, making it global implicitly
The last test ran on the "Get All Books List" spec fails the global deferred promise
On line 60 (in the "Get Single Book Detail" spec), deferredSuccess is declared with var making it local to the function passed to inject()
On line 70 (the test in question), where (I assume) you meant to reject the "Single Book" deferredSuccess, you're actually failing the global/list deferred promise. This has no effect, since as mentioned in item 2 that promise was already failed and Q ignores repeated rejections.
So, that should explain why the error is not what you think it should be.
deferred isn't the only variable with scoping issues in your example; those should be addressed. I suggest wrapping the file in an IFFE and using strict mode. It'll make the code more predictable and avoid issues like this.
Doing this will only get you halfway there; #estus's response should round out the job.

Related

Object is not defined when stubbing with Jasmine

I am very new to Jasmine. I am intending to use it for with vanilla javascript project. The initial configuration was a breeze but I am receiving object not defined error while using spyOn.
I have downloaded the version 3.4.0 Jasmine Release Page and added the files 'as is' to my project. I then have changed jasmine.json file accordingly and see the all the example tests passing. However when try spyOn on a private object, I am getting undefined error,
if (typeof (DCA) == 'undefined') {
DCA = {
__namespace: true
};
}
DCA.Audit = {
//this function needs to be tested
callAuditLogAction: function (parameters) {
//Get an error saying D365 is not defined
D365.API.ExecuteAction("bu_AuditReadAccess", parameters,
function (result) { },
function (error) {
if (error != undefined && error.message != undefined) {
D365.Utility.alertDialog('An error occurred while trying to execute the Action. The response from server is:\n' + error.message);
}
}
);
}
}
and my spec class
describe('Audit', function(){
var audit;
beforeEach(function(){
audit = DCA.Audit;
})
describe('When calling Audit log function', function(){
beforeEach(function(){
})
it('Should call Execute Action', function(){
var D365 = {
API : {
ExecuteAction : function(){
console.log('called');
}
}
}
// expectation is console log with say hello
spyOn(D365.API, 'ExecuteAction').and.callFake(() => console.log('hello'));
var params = audit.constructActionParameters("logicalName", "someId", 'someId');
audit.callAuditLogAction(params);
})
})
})
As you can see my spec class does not know about actual D365 object. I was hoping to stub the D365 object without having to inject it. Do I need to stub out whole 365 library and link it to my test runner html?
I got it working after some pondering. So the library containing D365 should still need to be added to my test runner html file. after that I can fake the method call like below,
it('Should call Execute Action', function(){
spyOn(D365.API, 'ExecuteAction').and.callFake(() => console.log('hello'));
var params = audit.constructActionParameters("logicalName", "someId", 'someId');
audit.callAuditLogAction(params);
})
it is now working.

In Electron, why can't i assign values from node modules to angularjs views?

I'm diving into Electron. I'm seeing some odd behavior:
Question:
Why is my object on the $scope not updating when i set data from a node module from within an angularjs controller?
Context:
I'm using the node module adb-kit to detect an external android device.
I'm using AngularJS 1.5.X to render my views
I'm using ui-router to set the scope around my view
Goal:
Display data from the android device inside an angularJS view
Code:
app.controller('DetectionController', function($scope, $state) {
console.log('DetectionController');
//node modules
var adb = require('adbkit');
var client = adb.createClient();
//AngularJS Render Scope
$scope.model = {
id: 11111,
attached: true
}
var forceUpdate = function(id) {
console.log('call', id);
console.log('id type is: ', typeof id)
$scope.model.id = id;
}
forceUpdate(22222);
client.trackDevices()
.then(function(tracker) {
tracker.on('add', function(device) {
forceUpdate(device.id); //this doesn't assign
console.log('Device %s was plugged in', device.id); //shows in console
});
tracker.on('end', function() {
console.log('Tracking stopped');
});
})
.catch(function(err) {
console.error('Something went wrong:', err.stack);
});
});
Console Output:
DetectionController
detct.js:15 call 22222
detct.js:16 id type is: number
detct.js:15 call 0168376B1701F01C
detct.js:16 id type is: string
detct.js:26 Device 0168376B1701F01C was plugged in
Expected Result:
The final call to forceUpdate() should assign a value of 0168376B1701F01C and update the view
Actual Result:
The previous assignment of 22222 is still reflected in the DOM
Ask: How do i correctly assign a value from the node module and get the browser to update?
i suspect the problem is with digest cycle not getting fired. Angular will trigger the digest cycle only during events recognized by angular. If you have custom event, then it is developers responsibility to let angular know to trigger digest cycle. Try changing the code to the following
client.trackDevices()
.then(function(tracker) {
tracker.on('add', function(device) {
forceUpdate(device.id); //this doesn't assign
console.log('Device %s was plugged in', device.id); //shows in console
$scope.$apply();
});
tracker.on('end', function() {
console.log('Tracking stopped');
});
})
.catch(function(err) {
console.error('Something went wrong:', err.stack);
});

Undefined : value.push(new Resource(item)

I'm trying to test a factory but I'm getting a weird error. I looked around but haven't been able to find a similar problem. Any idea on what I'm doing wrong?
TypeError: 'undefined' is not a function (evaluating 'value.push(new Resource(item))')
Using angluarjs v1.3.20
factory.js
'use strict';
//Business service used for communicating with the articles REST endpoints
angular.module('businesses').factory('Business', ['$resource',
function ($resource) {
return $resource('api/businesses/:businessId', {
businessId: '#_id'
}, {
update: {
method: 'PUT'
}
});
}
]);
test.js
describe('My Service', function () {
// Then we can start by loading the main application module
beforeEach(module(ApplicationConfiguration.applicationModuleName));
afterEach(inject(function($httpBackend){
//These two calls will make sure that at the end of the test, all expected http calls were made
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
}));
it('mock http call', inject(function($httpBackend, Business) {
var resource = new Business({
_id:'abcd'
});
var arraya = [{
_id:'abcd'
}, {
_id:'abcde'
}];
//Create an expectation for the correct url, and respond with a mock object
$httpBackend.expectGET('api/businesses/abcd').respond(200, arraya)
resource.$query();
//Because we're mocking an async action, ngMock provides a method for us to explicitly flush the request
$httpBackend.flush();
//Now the resource should behave as expected
console.log(resource);
//expect(resource.name).toBe('test');
}));
});
xxxxxxxxxxxxxxxxxxxxxxxxxxx
Shouldn't you only expect after you flushed?
resource.$query();
//Because we're mocking an async action, ngMock provides a method for us to explicitly flush the request
$httpBackend.flush();
//Create an expectation for the correct url, and respond with a mock object
$httpBackend.expectGET('api/businesses/abcd').respond(200, arraya);

Angular JS : Not able to test service function with promises in Jasmine

Here, _fact is a reference to the service.
it('Git Check', function() {
$scope.user = 'swayams'
var data;
_fact.Git($scope).then(function(d) {
expect(d.data.length).toEqual(4)
}, function() { expect(d).not.toBeNull(); });
});
I am getting the error
SPEC HAS NO EXPECTATIONS Git Check
Update
After forcing async as per #FelisCatus and adding $formDigest, I am getting a different error Error: Unexpected request: GET https://api.github.com/users/swayams/repos
No more request expected
The updated code snippet looks something like -
it('Git Check', function(done) {
$scope.user = 'swayams'
var data;
_fact.Git($scope).then(function(d) {
expect(d.data.length).toEqual(4)
}, function() { expect(d).not.toBeNull(); });
});
$rootScope.$formDigest();
I have a Plunk here illustrating the issue.
Jasmine is not seeing your expectations because your function returns before any expect() is called. Depending on your situation, you may want to use async tests, or use some promise matchers.
With async tests, you add an additional argument to your test function, done.
it('Git Check', function (done) {
$scope.user = 'swayams'
var data;
_fact.Git($scope).then(function(d) {
expect(d.data.length).toEqual(4);
}, function() { expect(d).not.toBeNull(); }).finally(done);
$rootScope.$digest();
});
(Note the finally clause in the end of the promise chain.)
Please note that you have to do $rootScope.$digest() for the promises to resolve, even if your code is not using it. See: How to resolve promises in AngularJS, Jasmine 2.0 when there is no $scope to force a digest?

Error: No pending request to flush - when unit testing AngularJs service

I'm newbie to AngularJs, and I'm in the process of writing my first unit test; to test the service I wrote a test that simply returns a single Json object. However, everytime I run the test I get the error stated in the title. I don't know what exactly is causing this! I tried reading on $apply and $digest and not sure if that's needed in my case, and if yes how; a simple plunker demo would be appreciated.
here is my code
service
var allBookss = [];
var filteredBooks = [];
/*Here we define our book model and REST api.*/
var Report = $resource('api/books/:id', {
id: '#id'
}, {
query: {
method: 'GET',
isArray: false
}
});
/*Retrive the requested book from the internal book list.*/
var getBook = function(bookId) {
var deferred = $q.defer();
if (bookId === undefined) {
deferred.reject('Error');
} else {
var books= $filter('filter')(allBooks, function(book) {
return (book.id == bookId);
});
if (books.length > 0) {
deferred.resolve(books[0]);//returns a single book object
} else {
deferred.reject('Error');
};
};
return deferred.promise;
};
test
describe('unit:bookService', function(){
beforeEach(module('myApp'));
var service, $httpBackend;
beforeEach(inject(function (_bookService_, _$httpBackend_) {
service = _bookService_;
$httpBackend = _$httpBackend_;
$httpBackend.when('GET', "/api/books/1").respond(200, {
"book": {
"id": "1",
"author": "James Spencer",
"edition": "2",
.....
}
});
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should return metadata for single report', function() {
service.getBook('1').then(function(response) {
expect(response.length).toEqual(1);
});
$httpBackend.flush();// error is in this line
});
});
error
Error: No pending request to flush !
at c:/myapp/bower_components/angular-mocks/angular-mocks.js:1439
at c:/myapptest/tests/bookTest.js:34
libs version
AngularJS v1.2.21
AngularJS-mock v1.2.21
I don't see where you're actually issuing a Report.query(). The getBook function just returns an unresolved promise that will never be resolved because nothing in the function is async.
You need to call Report.query via the book function with the promise resolved in the .then() (in the book function). After that, flush the http backend in the service.getBook().then() and do the expect.

Categories

Resources