Unit testing AngularJS promises with ES6 - angularjs

I'm trying to test an async method in an AngularJS service that calls another async function internally using Jasmine and Karma.
Here's how my service looks like:
export default class SearchUserAPI {
constructor(BaseService, $q) {
this.q_ = $q;
this.service_ = BaseService;
}
isActive(email) {
const params = {'email': email};
return this.service_.getUser(params).then(isActive => {
// This part cannot be reached.
console.log('Is Active');
// I need to test the following logic.
return isActive ? true : this.q_.reject(`User ${email} is not active.`);
});
}
}
And here's how my test looks like:
import SearchUserApi from './api.service';
let service,
mockedService,
$q;
const email = 'chuck.norris#openx.com';
const expectedParams = {email: email};
describe('Search API unit tests', function() {
beforeEach(inject(_$q_ => {
$q = _$q_;
mockedService = {};
service = new SearchUserApi(mockedService, $q);
}));
// This test passes, but it doesn't reach the logging statement in main method.
it('is verifying that Chuck Norris should be active', () => {
// Trying to mock getUser() to return a promise that resolves to true.
mockedService.getUser = jasmine.createSpy('getUser').and.returnValue($q.when(true));
service.isActive(email).then(result => {
// The following should fail, but since this part is called asynchronously and tests end before this expression is called, I never get an error for this.
expect(result).toBe(false);
});
// This test passes, but I'm not too sure how I can verify that isActive(email) returns true for user.
expect(mockedService.getUser).toHaveBeenCalledWith(expectedParams);
});
});
I see in a lot of tutorials, they talk about using $scope and apply to see if a scope variable has been changed. But in my case, I'm not manipulating any instance(scope) variable to use $scope.apply().
Any idea how I can make the test to wait for my async calls to be resolved before they end?

I figured out how to go through the async method. All I have to do was to inject $rootScope and use $rootScope.$digest() after I call the async method, even if I'm not touching scope variables inside my test.

Related

Spying on service dependencies

I'm curious about the best way to spy on dependencies so I can make sure that their methods are being called in my services. I reduced my code to focus on the problem at hand. I'm able to test my service fine, but I want to also be able to confirm that my service (In this case metricService) has methods that are also being called. I know I have to use createSpyObj in some way, but while the function is executing properly, the spyObj methods are not being caught. Should I even be using createSpyObj? Or should I use spyObj? I'm a but confused about the concept of spying when it concerns dependencies.
UPDATE: When using SpyOn I can see one method getting called, but other methods are not
Test.spec
describe("Catalogs service", function() {
beforeEach(angular.mock.module("photonServicesCommons"));
var utilityService, metricsService, loggerService, catalogService, localStorageService;
var $httpBackend, $q, $scope;
beforeEach(
inject(function(
_catalogService_,
_metricsService_,
_$rootScope_,
_$httpBackend_
) {
catalogService = _catalogService_;
$scope = _$rootScope_.$new();
$httpBackend = _$httpBackend_;
$httpBackend.when('GET', "/ctrl/catalog/all-apps").respond(
{
catalogs: catalogs2
}
);
metricsService = _metricsService_;
startScope = spyOn(metricsService, 'startScope')
emitSuccess = spyOn(metricsService, 'emitGetCatalogSuccess').and.callThrough();
endScope = spyOn(metricsService, 'endScope');
})
);
afterEach(function(){
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
describe('get catalog', function(){
it("Should get catalogs", function(done) {
catalogService.normalizedDynamicAppList = testDynamicAppList1;
catalogService.response = null;
var promise3 = catalogService.getCatalog();
promise3.then(function (res) {
expect(res.catalogs).toEqual(catalogs2);
});
expect(metricsService.startScope).toHaveBeenCalled();
expect(metricsService.emitGetCatalogSuccess).toHaveBeenCalled();
expect(metricsService.endScope).toHaveBeenCalled();
$scope.$digest();
done();
$httpBackend.flush();
});
});
});
Service
public getCatalog(): IPromise<Interfaces.CatalogsResponse> {
if (this.response !== null) {
let finalResponse:any = angular.copy(this.response);
return this.$q.when(finalResponse);
}
return this.$q((resolve, reject) => {
this.metricsService.startScope(Constants.Photon.METRICS_GET_CATALOG_TIME);
this.$http.get(this.catalogEndpoint).then( (response) => {
let data: Interfaces.CatalogsResponse = response.data;
let catalogs = data.catalogs;
if (typeof(catalogs)) { // truthy check
catalogs.forEach((catalog: ICatalog) => {
catalog.applications.forEach((application: IPhotonApplication) => {
if( !application.appId ) {
application.appId = this.utilityService.generateUUID();
}
})
});
} else {
this.loggerService.error(this.TAG, "Got an empty catalog.");
}
this.response = data;
this.metricsService.emitGetCatalogSuccess();
console.log("CALLING END SCOPE");
this.metricsService.endScope(Constants.Photon.METRICS_GET_CATALOG_TIME);
resolve(finalResponse);
}).catch((data) => {
this.loggerService.error(this.TAG, "Error getting apps: " + data);
this.response = null;
this.metricsService.emitGetCatalogFailure();
reject(data);
});
});
} // end of getCatalog()
Instead of using createSpyObj, you can just use spyOn. As in:
beforeEach(
inject(function(
_catalogService_,
_$rootScope_,
_$httpBackend_,
_metricsService_ //get the dependecy from the injector & then spy on it's properties
) {
catalogService = _catalogService_;
metricsService = _metricsService_;
$scope = _$rootScope_.$new();
...
// create the spy object for easy referral later on
someMethodSpy = jasmine.spyOn(metricsService, "someMethodIWannaSpyOn")
})
);
describe('get catalog', function(){
it("Should get catalogs", function(done) {
catalogService.normalizedDynamicAppList = testDynamicAppList1;
catalogService.response = null;
var promise3 = catalogService.getCatalog();
...other expects
...
//do the spy-related expectations on the function spy object
$httpBackend.flush(); // this causes the $http.get() to "move forward"
// and execution moves into the .then callback of the request.
expect(someMethodSpy).toHaveBeenCalled();
});
});
I've used this pattern when testing complex angular apps, along with wrapping external imported/global dependencies in angular service wrappers to allow spying and mocking them for testing.
The reason that createSpyObject won't work here is that using it will create a completely new object called metricService with the spy props specified. It won't be the same "metricService" that is injected into the service being tested by the angular injector. You want to get the actual same singleton service object from the injector and then spy on it's properties.
The other source of dysfunction was the $httpBackend.flush()s location. The $httpBackend is a mock for the $http service: you pre-define any number of expected HTTP requests to be made by the code you are testing. Then, when you call the function that internally uses $http to make a request to some url, the $httpBackend instead intercepts the call to $http method (and can do things like validate the request payload and headers, and respond).
The $http call's then/error handlers are only called after the test code calls $httpBackend.flush(). This allows you to do any kind of setup necessary to prep some test state, and only then trigger the .then handler and continue execution of the async logic.
For me personally this same thing happens every single time I write tests with $httpBackend, and it always takes a while to figure out or remember :)

How to test a method that uses async/await?

I've seen a lot of articles about how use async/await in your unit tests, but my need is the opposite.
How do you write a test for a method that uses async/await?
My spec is not able to reach any code after the 'await' line. Specifically, the spec fails in two ways.
1) HelloWorld.otherCall returns undefined instead of the return value I specify
2) HelloWorld.processResp never gets called
class HelloWorld {
async doSomething(reqObj) {
try {
const val = await this.otherCall(reqObj);
console.warn(val); // undefined
return this.processResp(val);
}
}
}
describe('HelloWorld test', function () {
let sut = new HelloWorld(); //gross simplification for demo purposes
describe('doSomething()', function () {
beforeEach(function mockInputs() {
this.resp = 'plz help - S.O.S.';
});
beforeEach(function createSpy() {
spyOn(sut, 'otherCall').and.returnValue( $q.resolve(this.resp) );
spyOn(sut, 'processResp');
});
it('should call otherCall() with proper arguments', function () {
//this test passes
});
it('should call processResp() with proper arguments', function () {
sut.doSomething({});
$rootScope.$apply(); //you need this to execute a promise chain..
expect(sut.processResp).toHaveBeenCalledWith(this.resp);
//Expected spy processResp to have been called with [ 'plz help SOS' ] but it was never called.
});
});
});
Running angular 1.5 and jasmine-core 2.6.
The .then of a promise is overloaded to handle either promises or values, and await is syntactic sugar for calling then.
So there is no reason your spy would be required to return a promise, or even a value. Returning at all, even if undefined, should trigger the await to fire, and kick off the rest of your async function.
I believe your problem is that you are not waiting for the doSomething promise to resolve before trying to test what it did. Something like this should get you more in the ballpark.
it('should call processResp() with proper arguments', async function () {
await sut.doSomething({});
// ...
});
Jasmine has Asynchronous Support. You can probably find a solution that way.
Personally, I think you should not test such methods at all.
Testing state means we're verifying that the code under test returns the right results.
Testing interactions means we're verifying that the code under test calls certain methods properly.
At most cases, testing state is better.
At your example,
async doSomething(reqObj) {
try {
const val = await this.otherCall(reqObj);
return this.processResp(val);
}
}
As long as otherCall & processResp are well covered by unit tests your good.
Do something should be covered by e2e tests.
you can read more about it at http://spectory.com/blog/Test%20Doubles%20For%20Dummies

Jasmine mocked promise method doesn't trigger then block

I'm having trouble getting a mocked method that returns a promise to trigger the then connected to it.
Here is the service being tested. The get method is the important aspect of it.
angular.module('service.overtimeRules').factory('OvertimeRules', OvertimeRules);
function OvertimeRules(_, Message, OvertimeRulesModel) {
const rules = [];
return { get };
function get() {
return OvertimeRulesModel.get().then(
data => {
console.log('This never fires :(');
rules.push(...data);
},
() => Message.add('Error retrieving the overtime rules.', 'danger')
);
}
}
Here is the spec file. The error I get is TypeError: undefined is not an object (evaluating 'OvertimeRulesModel.get'). If I remove the get describe wrapper, the error goes away, but the service method's then block still doesn't fire.
describe('OvertimeRules', () => {
let $q, $rootScope, deferred, Message, OvertimeRules, OvertimeRulesModel;
beforeEach(() => {
module('globals');
module('service.overtimeRules');
module($provide => {
$provide.value('Message', Message);
$provide.value('OvertimeRulesModel', OvertimeRulesModel);
});
inject((_$q_, _$rootScope_, _OvertimeRules_) => {
$q = _$q_;
$rootScope = _$rootScope_;
OvertimeRules = _OvertimeRules_;
deferred = { OvertimeRulesModel: { get: $q.defer(), update: $q.defer } };
Message = jasmine.createSpyObj('Message', ['add']);
OvertimeRulesModel = jasmine.createSpyObj('OvertimeRulesModel', ['get']);
OvertimeRulesModel.get.and.returnValue(deferred.OvertimeRulesModel.get.promise);
});
});
describe('get', () => { // why would this cause an error?
it('gets a list of rules and pushes them into the rules array', () => {
deferred.OvertimeRulesModel.get.resolve(['rules']);
OvertimeRules.get();
$rootScope.$digest(); // $apply doesn't work either
expect(OvertimeRules.rules).toEqual(['rules']);
});
});
});
Possibly important notes:
I'm happy to approach this another way as long as the code is concise and readable. Also, I need to reject promises as well.
OvertimeRulesModel is another factory in this service.overtimeRules module; Message is part of a separate module.
I tried moving OvertimeRulesModel to its own module, but got the same error.
I'm using the
A = jasmine.createSpyObj(...); A.get.and.returnValue(promise);
approach in a controller spec and everything is working fine
These are unit tests, so I'd prefer to avoid injecting the actual OvertimeRulesModel service.
Better answer
Seems like I was losing reference the variables being supplied to the module provider. Solution is just to move the createSpyObj calls above the $provide statement.
...
module('service.overtimeRules');
Message = jasmine.createSpyObj('Message', ['add']);
OvertimeRulesModel = jasmine.createSpyObj('OvertimeRulesModel', ['get']);
module($provide => {
$provide.value('Message', Message);
$provide.value('OvertimeRulesModel', OvertimeRulesModel);
});
...
Going with solution fixed another issue I ran into. Also, I didn't have to change the returnValue syntax.
Old answer:
And apparently, all I needed to do was change returnValue to callFake like so:
OvertimeRulesModel = jasmine.createSpyObj('OvertimeRulesModel', ['get']);
OvertimeRulesModel.get.and.callFake(() => deferred.OvertimeRulesModel.get.promise);

How to mock Google Analytics function call ga()

I have a service MyService with a function using the ga() event tracking call which I want to test:
angular.module('myModule').factory('MyService', [function() {
var myFunc = function() {
ga('send', 'event', 'bla');
// do some stuff
}
return {
myFunc: myFunc
}
]);
My spec file looks like this:
describe('The MyService', function () {
var MyService,
ga;
beforeEach(function () {
module('myModule');
ga = function() {};
});
beforeEach(inject(function (_MyService_) {
MyService = _MyService_;
}));
it('should do some stuff', function () {
MyService.myFunc();
// testing function
});
});
Running my tests always gives me:
ReferenceError: Can't find variable: ga
The problem is global scope of ga.
The ga variable that you create inside your tests has a local scope and will not be visible to your own service.
By using a global variable (ga) you have made unit testing difficult.
The current option would be to either create a angular service to wrap gaand use that everywhere else. Such service can be mocked too.
The other option is to override the global ga. But this will have side effects.
window.ga=function() {}
After trying different solution I finally fixed with below code.
beforeAll( ()=> {
// (<any>window).gtag=function() {} // if using gtag
(<any>window).ga=function() {}
})
Slightly out of date, but I am trying to leverage ReactGA and mocked creating an event like:
it('should do something...', () => {
const gaSpy = jest.spyOn(ReactGA, 'ga');
someService.functionThatSendsEvent({ ...necessaryParams });
expect(gaSpy).toHaveBeenCalledWith('send', 'event',
expect.objectContaining({/*whatever the event object is supposed to be*/}
);
});
This is helpful if youre sending specific data to an angular/reactjs service which is then sending it to GA.

chai-as-promised erroneously passes tests

I'm trying to write tests for a method that returns an angular promise ($q library).
I'm at a loss. I'm running tests using Karma, and I need to figure out how to confirm that the AccountSearchResult.validate() function returns a promise, confirm whether the promise was rejected or not, and inspect the object that is returned with the promise.
For example, the method being tested has the following (simplified):
.factory('AccountSearchResult', ['$q',
function($q) {
return {
validate: function(result) {
if (!result.accountFound) {
return $q.reject({
message: "That account or userID was not found"
});
}
else {
return $q.when(result);
}
}
};
}]);
I thought I could write a test like this:
it("it should return an object with a message property", function () {
promise = AccountSearchResult.validate({accountFound:false});
expect(promise).to.eventually.have.property("message"); // PASSES
});
That passes, but so does this (erroneously):
it("it should return an object with a message property", function () {
promise = AccountSearchResult.validate({accountFound:false});
expect(promise).to.eventually.have.property("I_DONT_EXIST"); // PASSES, should fail
});
I am trying to use the chai-as-promised 'eventually', but all my tests pass with false positives:
it("it should return an object", function () {
promise = AccountSearchResult.validate();
expect(promise).to.eventually.be.an('astronaut');
});
will pass. In looking at docs and SO questions, I have seen examples such as:
expect(promise).to.eventually.to.equal('something');
return promise.should.eventually.equal('something');
expect(promise).to.eventually.to.equal('something', "some message about expectation.");
expect(promise).to.eventually.to.equal('something').notify(done);
return assert.becomes(promise, "something", "message about assertion");
wrapping expectation in runs() block
wrapping expectation in setTimeout()
Using .should gives me Cannot read property 'eventually' of undefined. What am I missing?
#runTarm 's suggestions were both spot on, as it turns out. I believe that the root of the issue is that angular's $q library is tied up with angular's $digest cycle. So while calling $apply works, I believe that the reason it works is because $apply ends up calling $digest anyway. Typically I've thought of $apply() as a way to let angular know about something happening outside its world, and it didn't occur to me that in the context of testing, resolving a $q promise's .then()/.catch() might need to be pushed along before running the expectation, since $q is baked into angular directly. Alas.
I was able to get it working in 3 different ways, one with runs() blocks (and $digest/$apply), and 2 without runs() blocks (and $digest/$apply).
Providing an entire test is probably overkill, but in looking for the answer to this I found myself wishing people had posted how they injected / stubbed / setup services, and different expect syntaxes, so I'll post my entire test.
describe("AppAccountSearchService", function () {
var expect = chai.expect;
var $q,
authorization,
AccountSearchResult,
result,
promise,
authObj,
reasonObj,
$rootScope,
message;
beforeEach(module(
'authorization.services', // a dependency service I need to stub out
'app.account.search.services' // the service module I'm testing
));
beforeEach(inject(function (_$q_, _$rootScope_) {
$q = _$q_; // native angular service
$rootScope = _$rootScope_; // native angular service
}));
beforeEach(inject(function ($injector) {
// found in authorization.services
authObj = $injector.get('authObj');
authorization = $injector.get('authorization');
// found in app.account.search.services
AccountSearchResult = $injector.get('AccountSearchResult');
}));
// authObj set up
beforeEach(inject(function($injector) {
authObj.empAccess = false; // mocking out a specific value on this object
}));
// set up spies/stubs
beforeEach(function () {
sinon.stub(authorization, "isEmployeeAccount").returns(true);
});
describe("AccountSearchResult", function () {
describe("validate", function () {
describe("when the service says the account was not found", function() {
beforeEach(function () {
result = {
accountFound: false,
accountId: null
};
AccountSearchResult.validate(result)
.then(function() {
message = "PROMISE RESOLVED";
})
.catch(function(arg) {
message = "PROMISE REJECTED";
reasonObj = arg;
});
// USING APPLY... this was the 'magic' I needed
$rootScope.$apply();
});
it("should return an object", function () {
expect(reasonObj).to.be.an.object;
});
it("should have entered the 'catch' function", function () {
expect(message).to.equal("PROMISE REJECTED");
});
it("should return an object with a message property", function () {
expect(reasonObj).to.have.property("message");
});
// other tests...
});
describe("when the account ID was falsey", function() {
// example of using runs() blocks.
//Note that the first runs() content could be done in a beforeEach(), like above
it("should not have entered the 'then' function", function () {
// executes everything in this block first.
// $rootScope.apply() pushes promise resolution to the .then/.catch functions
runs(function() {
result = {
accountFound: true,
accountId: null
};
AccountSearchResult.validate(result)
.then(function() {
message = "PROMISE RESOLVED";
})
.catch(function(arg) {
reasonObj = arg;
message = "PROMISE REJECTED";
});
$rootScope.$apply();
});
// now that reasonObj has been populated in prior runs() bock, we can test it in this runs() block.
runs(function() {
expect(reasonObj).to.not.equal("PROMISE RESOLVED");
});
});
// more tests.....
});
describe("when the account is an employee account", function() {
describe("and the user does not have EmployeeAccess", function() {
beforeEach(function () {
result = {
accountFound: true,
accountId: "160515151"
};
AccountSearchResult.validate(result)
.then(function() {
message = "PROMISE RESOLVED";
})
.catch(function(arg) {
message = "PROMISE REJECTED";
reasonObj = arg;
});
// digest also works
$rootScope.$digest();
});
it("should return an object", function () {
expect(reasonObj).to.be.an.object;
});
// more tests ...
});
});
});
});
});
Now that I know the fix, it is obvious from reading the $q docs under the testing section, where it specifically says to call $rootScope.apply(). Since I was able to get it working with both $apply() and $digest(), I suspect that $digest is really what needs to be called, but in keeping with the docs, $apply() is probably 'best practice'.
Decent breakdown on $apply vs $digest.
Finally, the only mystery remaining to me is why the tests were passing by default. I know I was getting to the expectations (they were being run). So why would expect(promise).to.eventually.be.an('astronaut'); succeed? /shrug
Hope that helps. Thanks for the push in the right direction.

Resources