Manage angularjs controller init with karma tests - javascript

Ive seen such questions and they all say 'extract logic out into a service and mock the service'. Simple, except i have as much as possible.
So when the controller inits i have a call to $scope.getWidgets() this method calls widgetService to get a list of widgets for the user, it will then display a toast notification using notifyService. Should widgetService reject its promise or the user have no widgets a call to notifyService is made. This is all done when the controller inits.
$scope.getWidgets = function () {
widgetService.getWidgets()
.then(function (widgets) {
if (widgets.length === 0) {
notifyService.notify('no widgets');
}
$scope.widgets = widgets;
})
.catch(function (e) {
notifyService.notify('oh noes');
});
}
//called at bottom of controller
$scope.getWidgets();
Now all my tests so for have not had need to run any digest cycles, run running promises etc. The method im trying to test calls a service to save a widget. Should it succeed or fail it will again call notifyService to send a notification to the user. I'm testing that the notification is triggered using karma's spyOn and also testing some flags are set correctly. There's no real logic, thats done in the service.
$scope.saveWidget = function (widget) {
widgetService.saveWidget(widget)
.then(function () {
notifyService.notify('all dandy');
//set some scope flags, no logic
})
.catch(function (e) {
notifyService.notify('oh noes');
//set some more scope flags, no logic
});
}
So now i have have to run the digest cycle to trigger my promises. The Trouble is my mock for widgetService.getWidgets() returns a promise. So when i run the digest first it resolves the promise registered in my init, which calls the notify service, which activates my spyon, concluding my test with false data.
Here are my tests, (please forgive the big code block, ive tried to strip it down as much as possible).
describe('tests', function () {
var scope, controlelr, _mockWidgetService, _mockNotifyService;
beforeEach(function () {
//Init the service mocks, angular.noop for methods etc
_mockWidgetService = init();
_mockNotifyService = init();
});
beforeEach(function () {
//Regisetr hooks to pull in my mocked services
angular.mock.module('myModule');
angular.mock.module(function($provide) {
$provide.value('widgetService', _mockWidgetService);
$provide.value('notifyService', _mockNotifyService);
})
});
angular.mock.inject(function ($controller, $rootScope, widgetService, notifyService) {
//create scope and inject services to create controller
scope = $rootScope.$new();
//Trouble is $scope.getWidgets() is called now.
controller = $controller('testController', {
$scope: scope,
widgetService: widgetService,
notifyService: notifyService
});
});
describe('getWidgets', function() {
it('myTest', function () {
//I cannow manipulate mock services moreso if needed just for this test
spyOn(_mockNotifyService, 'notfy');
//Call save widget
scope.saveWidget();
scope.$digest();
expect(_mockNotifyService.notify).toHaveBeenCalledWith(//values);
});
});
});
So as you can see im struggling, i could mock all the mocks required for the init code, but id rather only mock the services i need for that test. Plus i don't like the idea of the init code running every single test potentially causing false errors (it has its own tests).
Any advise or solutions to such use cases would greatly appreciated.

I have found a potential solution that seems to work. I'm not entirely sure its best practice so if there are better options id be very grateful to hear them.
Essentially instead of mocking up my widgetService.getWidgets with a resolved promise before every test i mock it up with a deferred promise which never resolves. Idea being for tests that do not require it the init code will never run as the promise never resolves.
Tests that do require it to run just overwrite the mock and have widgetService.getWidgets return what is required.
That did mean some small changes, the controller had to be instantiated manually for each test (using beforeEach where available) instead of at the top of the very first describe).

Related

Angular Unit Test, SpyOn stand-alone function

I am using Jasmine to test an Angular app and would like to test that the getItem() function within my controller is called when the ready() function of the controller is called.
--- Controller ---
var vm = this;
vm.items = [];
$ionicPlatform.ready(ready);
function ready() {
vm.items.push(getItem());
function getItem(){
var item = //do stuff to get item;
console.log('getItem called');
return item;
}
}
--- Spec ---
describe('Controller', function(){
//--- Load app with dependencies and module to test code omitted.
beforeEach(function(){
//How do I spy on getItem() to test that it was called?
//I've tried getItem = jasmine.createSpy()
//I've tried spyOn(window, 'getItem')
}
//--- Initialize the controller and a mock scope code omitted.
beforeEach(function(done){
$ionicPlatform.ready(function(){
done();
});
});
it('getItem function should get called', function(){
expect(getItem).toHaveBeenCalled();
//--- Note, getItem does not get called according to the expect statement,
//--- but does output 'getItem called' to the terminal when running the test.
});
});
Unfortunately, you've come upon a fundamental limit of Javascript unit testing with Jasmine-- you can only spyOn methods that are exposed on some object. If there is a function that is internal to another function, and not exposed in anyway, you cannot test it directly.
However, you do have two options available to you:
Expose the function in a way that it can be spied on (generally as a method of whatever Angular component you are testing).
Test it indirectly.
The first is probably relatively self-evident, but the latter may be a little confusing. Basically, you can't test directly if the getItems function is called, but the function may have downstream methods it calls or values it changes you can test. For instance, you can test that vm.items.push is larger after ready is called, or you can spyOn(console.log) and expect(console.log).toHaveBeenCalledWith('getItem called').
You can find arguments for both approaches on the internet-- I tend to prefer approach two because I don't like doing refactors solely for the purpose of testability, but many will argue that refactoring for testability generally yields better code. That choice is yours to make. Hope this helps!

CallThrough injected spy

I'm doing some unitTests and my scenario is the following. I have like 50 tests whose call to a service function must be the same, but for one single test It will be so helpfull if I can call the original method. I tried with the and.callThrough but It's not working correctly. I'm trying to override the spy too but I can't. What I'm doing wrong?
beforeEach(inject(function($controller, _myService_){
spyOn(_myService_, 'getSomeData').and.callFake(function(data, params){
return dummyData;
});
createController = function() {
return $controller('MyCtrl',{
$uibModalInstance: modalInstance,
myService: _myService_,
injectedData: injectedData
});
};
}));
This is my test case.
it('My test case', function(){
controller = createController();
controller.myService.getSomeData = jasmine.createSpy().and.callThrough()
});
I'm using jasmine 2.0 and that test case is continuously calling the callFake function.
thanks
jasmine.createSpy().and.callThrough() is unaware of the spied method and there's no way how it can know about it, calling it just results in calling a noop function.
Spying strategy can be changed for existing spies,
controller.myService.getSomeData.and.callThrough();

AngularJS Service - Make $resource return data right away instead of a promise so clients don't have to use .then()

This is a simple problem but seems tricky due to asynchronous nature of promises.
With in my data service, I want to ensure that data is retrieved back from the server before moving on to next step as other functions/clients depend on that data.
I don't want clients to register callbacks or use .then() and certainly not use $timeout when requesting the data.
How can I make sure that the controller and service that depend on my Data service get the data right away when requested? I've explained my problem using the code below. I would very much like to continue on my current approach if possible.
AngularJS Version: 1.4+
My Current Approach
//Data Service
App.factory('HealthyFoodService', function($resource, $q) {
var fruitsPromise = $resource('api/food/fruits', {}).query().$promise;
var veggiesPromise = $resource('api/food/veggies',{}).query().$promise;
var fruitsData, veggiesData;
//Ensure $q populates the data before moving on to next statement.
$q.all([fruitsPromise, veggiesPromise]).then(function(data) {
fruitsData = data[0];
veggiesData = data[1];
}
function getCitrusFruits() {
var citrusFruits;
var allFrutis = fruitsData;
//code breaks here because fruitsData is still undefined when called from a controller or another service.
//some logic for this function
return citrusFruits;
}
function getLeafyVeggies() {
var leafyVeggies;
var allVeggies = veggiesData;
//code breaks here because veggieData is still undefined when called from a controller or another service.
//some logic for this function
return leafyVeggies;
}
function getLeafyVeggyByName(name) {
//this function is called from other services and controllers.
var leafyVeggies = getLeafyVeggies();
return leafyVeggies[name];
}
return {
getCitrusFruits: getCitrusFrutis,
getLeafyVeggies: getLeafyVeggies,
getLeafyVeggyByName: getLeafyVeggyByName
});
Below are the two clients. One is a controller and another is a service. They both need the data right away as following statements depend on the returned data.
//Controller
App.controller('LeafyVeggieController', function(HealthyFoodService) {
//Ideally I just'like to do something like below instead of calling `.then()` and registering callbacks.
var leafyVeggies = FoodService.getLeafyVeggies();
//leafyVeggies is undefined because data is not available yet;
});
//Another service depending on HealthyFoodService- similar scenario
App.factory('LeafyVeggieReportService', function(HealthyFoodService) {
function generateLeafyVeggieReport() {
var name = 'spinach';
var veggieInfo = HealthyFoodService.getLeafyVeggieByName(spinach);
//veggieInfo is undefined
//logic that need data.
});
My Previous Approach
Below is how I had it partially working before but I wasn't happy about using .then() everytime I needed the data.(Even with in the same service)
App.factory('HealthyFoodService', function($resource, $q) {
//resource variables;
function getLeafyVeggies() {
return $q.all([veggiesPromise]).then(function(data) {
//logic
return leafyVeggies;
});
}
function getLeafyVeggieByName() {
var leafyVeggies = getLeafyVeggies().then(function(data) {
return data;
}
//some logic
//still causes issues when called from another service because above call doesn't get the data right away.
}
return {
getLeafyVeggies: getLeafyVeggies,
getLeafyVeggieByName: getLeafyVeggieByName
}
//controller
App.controller('LeafyVeggieController', function(HealthyFoodService) {
var leafyVeggies = HealthyFoodService.getLeafyVeggies().then(function(data) {
return data;
});
//controller related logic
});
Update
I'm using ui-router as well, so I'm aware that I can use resolve:{} in $stateProvider to inject the data directly into the controller. The puzzle is how to get the data when I make a request from another service or from another function with in the same service without having to use .then().
Solution
Using $q.all([]) in my client services that depend on my Data service has done the trick for me. I have used $q.all([]) whenever I'm in a situation where I need all the data to be present before start processing the logic.
I still have to use .then() on my clients, but by using $q.all([]), I can still slightly simulate a synchronous flow without breaking any asynchronous principles.
I don't think this is possible. There is inherent latency in network operations that you need to wait for. Not doing so results in the application continuing before the data is available.
That being said, most of the native model binding operations will implicitly wait on promises, so there would be no need to .then if no further manipulation of the data is necessary before passing to the view. You can also use the transformResponse method of ngResource to help with this.
Another option might be to shift the complexity to the resolve methods in the route config. In that case you would handle the .then in the resolve and pass the resolved data to your controller. This will keep your controllers cleaner, but still requires you resolve the promises in the route config.
Try having the service hold your data and have the controller reference that data, instead of trying to pass it to your controller scope. Resolve the promise inside the service like so:
App.factory("HealthyFoodService", function($resource,$q) {
var service = {};
service.data = {
fruitsData: [],
veggiesData: []
}
$resource('api/food/fruits', {}).query().$promise.then(function(data) {
$service.data.fruitsData = data[0];
$service.data.veggiesData = data[1];
})
service.getCitrusFruits = function() {
var citrusFruits;
// Perform some logic on service.data.fruitsData
return citrusFruits;
}
return service;
})
In you controller, you can talk to the service like so:
App.controller("FruitController", function($scope, HealthyFoodService) {
// Now you can access the service data directly via $scope.hfs.fruitsData
$scope.hfs = HealthyFoodService.data;
// Or we can create a local copy of the data using the getCitrusFruits function
$scope.citrusFruits = HealthyFoodService.getCitrusFruits();
})

Integrating non-Angular code?

I'm developing a Cordova/PhoneGap app, and I'm using the $cordovaPush plugin (wrapped for PushPlugin) to handle push notifications.
The code looks something like this:
var androidConfig = {
"senderID" : "mysenderID",
"ecb" : "onNotification"
}
$cordovaPush.register(androidConfig).then(function(result) {
console.log('Cordova Push Reg Success');
console.log(result);
}, function(error) {
console.log('Cordova push reg error');
console.log(error);
});
The "ecb" function must be defined with window scope, ie:
window.onNotification = function onNotification(e)...
This function handles incoming events. I'd obviously like to handle incoming events in my angular code - how can I integrate the two so that my onNotification function can access my scope/rootScope variables?
Usually, you'll wrap your 3rd party library in a service or a factory, but in the spirit of answering your particular scenario...
Here's one possibility:
angular.module('myApp').
controller('myController', function($scope, $window) {
$window.onNotification = function() {
$scope.apply(function() {
$scope.myVar = ...updates...
});
};
});
A couple of things to notice:
Try to use $window, not window. It's a good habit to get into as it will help you with testability down the line. Because of the internals of Cordova, you might actually need to use window, but I doubt it.
The function that does all of the work is buried inside of $scope.apply. If you forget to do this, then any variables you update will not be reflected in the view until the digest cycle runs again (if ever).
Although I put my example in a controller, you might put yours inside of a handler. If its an angular handler (ng-click, for example), you might think that because the ng-click has an implicit $apply wrapping the callback, your onNotification function is not called at that time, so you still need to do the $apply, as above.
...seriously... don't forget the apply. :-) When I'm debugging people's code, it's the number one reason why external libraries are not working. We all get bit at least once by this.
Define a kind of a mail controller in body and inside that controller use the $window service.
HTML:
<body ng-controller="MainController">
<!-- other markup .-->
</body>
JS:
yourApp.controller("BaseController", ["$scope", "$window", function($scope, $window) {
$window.onNotification = function(e) {
// Use $scope or any Angular stuff
}
}]);

Why does Angular need wrappers?

One thing I still don't understand about Angular is...
Why use $window when i could just use the window global object and get the same result? Why use $timeout when I could use setTimeout, etc.
I use this native javascript code sometimes and it works just fine, so why did AngularJS created these wrappers in the first place?
It is integrated into the digest cycle (will trigger the HTML compiler and the DOM refreshes). Also makes the code easier to test because you can mock the $timeout object and test that it was called.
For example with $timeout, you can call $timeout.flush() in your unit tests and it will act as if the timeout waited the appropriate amount of time and trigger the callback. This makes your tests run much faster which is also good for TDD.
Here is a simple async example - assume that asyncThing.method() uses $timeout and $log to output a message
describe('Async test', function () {
var asyncThing, $timeout, $log;
beforeEach(module('async'));
beforeEach(inject(function (_asyncThing_, _$timeout_, _$log_) {
asyncThing = _asyncThing_;
$timeout = _$timeout_;
$log = _$log_;
}));
it('should do some async stuff', function () {
asyncThing.method(some_arguments);
$timeout.flush();
expect($log.info.logs).toContain(['Some output']);
});
});

Categories

Resources