Angular unit Testing spyOn().and.callthrough doesn't call actual funciton - angularjs

I am new in unit testing with Jasmine in Angular.
I am currently testing a service that has a function called loadSomething(id) and I have added a console.info in it.
MY SERVICE:
function loadSomething(id)
{
console.info('this is a test message');
return (a promise from a POST request)
}
And this is my test (spec) file:
//verify that the load function exists
it('load snapshot',function(){
expect(MyService.loadSomething(108)); //statement 1
spyOn(MyService, 'loadSomething').and.callThrough(); //statement 2
});
So, I read online that the callthrough method of SpyOn calls the ACTUAL function.
However, when i run my test with the expect statement (statement 1) the console.info message is invoked (that works fine). On the other hand, when I comment out statement 1 and uncomment the SpyOn(statement 2) I do not get the console.info message anymore.
I would expect the exact opposite to happen.
Have I understood something wrong here?
(the rest of the code works fine, both the spec file and the actual service, I just don't really get this specific thing)

The SpyOn will help you to setup how the function should react when it's being called upon in your tests. Basically it's jasmines way of creating mocks.
In your case you have defined what the test should do when the service function is being called, which is callThrough.
The problem is that you also need to act on the service function (or the scope function which calls your service method) in order
to trigger the SpyOn which will callThrough
it('load snapshot',function(){
//setup
spyOn(MyService, 'loadSomething').and.callThrough(); //statement 2
//act
//either call the scope function which uses the service
//$scope.yourServiceCallFunction();
//or call the service function directly
MyService.loadSomething(1); //this will callThrough
});
Here's an simple test where we will mock the response of the SpyOn to a string
it('test loadSomething',function(){
//setup
spyOn(MyService, 'loadSomething').and.returnValue('Mocked');
//act
var val = MyService.loadSomething(1);
//check
expect(val).toEqual('Mocked');
});

Related

TypeError: undefined is not a constructor in jasmine unit test with angularjs

I am having above error in my Jesmine tests. Code has a Controller which calls AppsService service to receive a promise response of array of apps followed by onLoad method -
AppsService.initAndGetApps(appCredentialType)
.then(onLoad)
.catch(onError);
Code works fine but getting error for unit tests like -
TypeError: undefined is not a constructor (evaluating 'AppsService.initAndGetApps(appCredentialType).then(onLoad)')
I tried mocking initAndGetApps in my spec file with Jasmine & also as custom method like this (both are giving same above error) -
1.
initAndGetApps : jasmine.createSpy("initAndGetApps").and.callFake(function (credentialType) {
return []; // empty is ok
}
2.
AppsService = {
initAndGetApps: function (credentialType) {
return [];
}
}
$provide.value('AppsService', AppsService);
AppsService uses deferred.promise to return promise response after doing some computation based on credentialType parameter (It also does two Promise calls). Test is not able to call initAndGetApps as not getting console of credentialType at first line of initAndGetApps.
Can someone guide me the right way to mock AppsService.
You are not returning a promise in none of those two mocks. I would try if something along these lines:
initAndGetApps : jasmine.createSpy("initAndGetApps").and.returnValue(
new Promise(function(resolve, reject) {
// not taking our time to do the job
resolve(desiredReturnValue);
}
);
You can change the return value to a mock value so your code works.

Promises are not resolved in Jasmine using Karma

I have a problem with a small karma unit test that should check a simple decryption/encryption service.
The thing is, if I call the following code "manual" (i.e., within my running angular app) everything is fine and I receive the expected test output:
this.encryptDataAsync('Hello World of Encryption','b4b63cd1a64dbef72fefe2eb3e3fc3eb').then((encryptedValue : string) : void => {
console.log('1',encryptedValue);
this.decryptDataAsync(encryptedValue,'b4b63cd1a64dbef72fefe2eb3e3fc3eb').then(function(decryptedValue : string) : void{
console.log('2',decryptedValue);
});
});
As soon as I try to run this Karma/Jasmine unit test
describe('simple encryption/decryption', function() {
var results = '';
beforeEach(function(done) {
_cryptoService.encryptDataAsync('ABC','b4b63cd1a64dbef72fefe2eb3e3fc3eb').then(function (encryptedValue){
console.log('1');
_cryptoService.decryptDataAsync(encryptedValue,'b4b63cd1a64dbef72fefe2eb3e3fc3eb').then(function(decryptedValue){
console.log('2');
results = decryptedValue;
done();
});
});
});
it("check results", function(done){
expect(results).toBe('ABC');
done();
}, 3000);
});
I never reach console.log('1') nor '2'. I can confirm this while debugging the unit test. However, this is the only unit test that fails in the complete suite, so I guess it won't by a problem with modules, etc.
Is there a general problem with my test case? I would have expected that I can use the then functions to handle my test case and, afterwards, call the done() function to invoke the assertion part.
Update/Edit:
The service uses webcrypto as a library. It is complete independent of angular besides being an angular service (so, no variables on scopes, etc)
I needed to call scope.apply since "$q is integrated with the $rootScope.Scope Scope model observation mechanism in angular, which means faster propagation of resolution or rejection into your models and avoiding unnecessary browser repaints, which would result in flickering UI."

Testing a filter used within a controller function

I have the following test case:
it('should return id if the post is successful',function(){
var result = {
id : "123"
};
ctrl.saveCallback(result);
expect(ctrl.method.id).to.equal("123");
});
Where ctrl.saveCallback copies the result.id into the method.id on the ctrl and then shows the success banner. On the success banner, we are using the translate filter to translate the message before showing it.
Function:
.....
ctrl.method.id = result.id;
magicallyShowOnScreen($filter('translate')('MESSAGES.SUCCESS'));
....
magicallyShowOnScreen is a service that shows whatever string we pass onto the screen, and that has been injected into beforeEach.
Can someone please point in the right direction as to how should I test or mock out this $filter('translate') ?
Preface: I'm not familiar with Mocha.js or how one would create spies however you would still inject them or similar mock objects the same way as you would for other testing frameworks.
Below is a Jasmine example, I hope it helps.
When you bootstrap your module (using the module / angular.mocks.module) function, you should provide your own version of $filter which should be a mock / spy that returns another mock / spy. For example
var $filter, filterFn;
beforeEach(module('myModule', function($provide) {
$filter = jasmine.createSpy('$filter');
filterFn = jasmine.createSpy('filterFn');
$filter.and.returnValue(filterFn);
$provide.value('$filter', $filter);
});
Then, in your test, you can make sure $filter is called with the right arguments, eg
expect($filter).toHaveBeenCalledWith('translate');
expect(filterFn).toHaveBeenCalledWith('MESSAGE.SUCCESS');
expect(magicallyShowOnScreen).toHaveBeenCalled(); // assuming this too is a spy

How to avoid the 'Error: Unexpected request: GET' every time an AngularJS test does a rootScope.digest() or httpBackend.flush()

All my UNIT tests, not E2E tests, that do an explicit rootScope.digest() or httpBackend.flush() to flush the asynchronous callback, experience the error:
How to avoid the 'Error: Unexpected request: GET'
No more request expected
I reckon it is because httpBackend calls the ui-router template. I don't know why it wants to do so. I'm not asking for this. I only want it to call my mocked json service.
This error forces me to have the following statement in each it() block:
$httpBackend.whenGET(/\.html$/).respond('');
There must be a neater way.
Specially if the test has no use of the $httpBackend in the first place:
it('should return the list of searched users', function() {
// Always use this statement so as to avoid the error from the $http service making a request to the application main page
$httpBackend.whenGET(/\.html$/).respond('');
var users = null;
UserService.search('TOTO', 1, 10, 'asc', function(data) {
users = data.content;
});
$rootScope.$digest();
expect(users).toEqual(RESTService.allUsers.content);
});
The test passes but it looks hackish. Or noobish :-)
EDIT: Another test:
it('should return the list of users', function () {
// Always use this statement so as to avoid the error from the $http service making a request to the application main page
$httpBackend.whenGET(/\.html$/).respond('');
// Create a mock request and response
$httpBackend.expectGET(ENV.NITRO_PROJECT_REST_URL + '/users/1').respond(mockedUser);
// Exercise the service
var user = null;
RESTService.User.get({userId: 1}).$promise.then(function(data) {
user = data;
});
// Synchronise
$httpBackend.flush();
// Check for the callback data
expect(user.firstname).toEqual(mockedUser.firstname);
expect(user.lastname).toEqual(mockedUser.lastname);
});
This is obviously by design, your tests should be checking that HTTP calls are being made and that they're requesting the correct URL. Instead of checking whether requests are made to /\.html$/ why not instead check whether requests are made to the correct endpoints? Whether that be a directives partial or an API call.
If you insist on throwing away what could be a useful test, you could move your whenGET() to a beforeEach().

Mocking $httpBackend - how to handle "Unexpected request, No more request expected"?

I have a Jasmine test that is coded like this:
it ("should send correct message to server to get data, and correctly set up scope when receiving it", function(){
$httpBackend.when('GET', 'https://localhost:44300/api/projectconfiguration/12').respond(fakedDtoBase);
$routeParams.projectId=fakeId; // user asks for editing project
scope.$apply(function(){
var controller=controllerToTest(); // so controller gets data when it is created
});
expect(scope.projectData).toEqual(fakedDtoBase);
});
and it kind of works, but I get the error:
Error: Unexpected request: GET views/core/main/main.html
No more request expected
at $httpBackend (C:/SVN/src/ClientApp/client/bower_components/angular-mocks/angular-mocks.js:1207:9)
at sendReq (C:/SVN/src/ClientApp/client/bower_components/angular/angular.js:7800:9)
at $http.serverRequest (C:/SVN/src/ClientApp/client/bower_components/angular/angular.js:7534:16)
(more stack trace)....
I do realise that I can mock every other call. But let's say I do not care what else my test wants to load as it may call few other things.
How I can make sure that every other requests just "happen silently", maybe offering a single dummy response for everything else?
Your test fails because a request is made which you haven't specified.
Try to add:
$httpBackend.when('GET', 'views/core/main/main.html').respond(fakedMainResponse);
Of course you should also define fakedMainResponse.
Please take a look also at the documentation (section Request Expectations vs Backend Definitions) which says:
Request expectations provide a way to make assertions about requests
made by the application and to define responses for those requests.
The test will fail if the expected requests are not made or they are
made in the wrong order.
The second paramete of $httpBackend.when is actually a RegExp. So if you provide a RegExp that will match all other requests it should work.
For those who are using the httpBackend to mock http calls in EndToEnd tests or just mocking the entire http calls for the application the solution is to add the following code in the app config section (change the regexp according your template's location):
$httpBackend.whenGET(/^\/templates\//).passThrough();
Reference: https://docs.angularjs.org/api/ngMockE2E/service/$httpBackend
Tested with angularjs 1.4 to fix similar problem while integrating ui-router
I think it's also important to notice that if you have a $digest(), your expectation should follow the $digest, like so:
_$rootScope_.$digest();
$httpBackend.when('GET', 'views/core/main/main.html').respond(fakedMainResponse);
// ...
$httpBackend.flush(); // And remember to flush at the end
you only need to add setTimeout and done property to your flush to prevent it
it('should get data in callback funcion', function (done) {
$httpBackend.whenGET(/\/my-endpoint/).respond(mockDataResponse);
apiFactory.getCurrencyFormat('en', function (res, err) {
expect(res.a).to.deep.equal(generalMock.a);
expect(res.b).to.deep.equal(generalMock.b);
});
setTimeout(function () {
done();
$httpBackend.flush();
}, 200);
});

Resources