AngularJS - How can i refactore my Jasmine specs? - javascript

I use AngularJS 1.2.16 with Jasmine 2.X for my javascript specs.
But they quickly turned messy. I am having a hard time finding information on how to refactor and structure the specs.
Here are some bad specs of mine:
channel = mockRestangular = $httpBackend = deferred = undefined
channel_id = {...}
beforeEach ->
module("channels", ($provide) ->
mockRestangular = {
configuration: { baseUrl: "" }
one: ->
this
post: ->
this
put: ->
this
...
}
module ($provide) ->
$provide.value('Restangular', mockRestangular)
return
)
beforeEach inject((_channel_, $q, $injector) ->
channel = _channel_
$httpBackend = $injector.get('$httpBackend')
deferred = $q.defer()
)
it "spec1", inject(($q, $rootScope) ->
deferred = $q.defer()
spyOn(mockRestangular.one().one(), 'get').and.returnValue(deferred.promise)
spyOn(channel::, 'init').and.stub()
new_channel = new channel(channel_id)
new_channel.updateCount()
deferred.resolve({"channels":[{...long...long...object...}]})
$rootScope.$digest()
expect(new_channel.meta.totalProducts).toEqual(24849)
expect(new_channel.meta.activeProducts).toEqual(1349)
)
it "spec2", inject(($q, $rootScope) ->
deferred = $q.defer()
spyOn(mockRestangular.one().one(), 'get').and.returnValue(deferred.promise)
spyOn(channel::, 'init').and.stub()
new_channel = new channel(channel_id)
new_channel.updateStatisticsRevenue()
deferred.resolve({"revenue_statistics":[{...another...very...very...long...object...}]})
$rootScope.$digest()
expect(new_channel.statistics.revenue).toEqual([{...kinda...long...object...result...}])
)
# spec with real respond-mock objects
describe "describtor2", ->
it "spec3", inject(($rootScope) ->
$httpBackend.expectPUT().respond(201,
{products:[{"sku":"10413161","active":false,"min_price":{"fractional":400,"currency":"EUR"},"max_price":{"fractional":950,"currency":"EUR"}},{"sku":"10413162","active":true,"min_price":{"fractional":458,"currency":"EUR"},"max_price":{"fractional":799,"currency":"EUR"}}]})
spyOn(mockRestangular.one().one(), 'get').and.returnValue(deferred.promise)
spyOn(channel::, 'init').and.stub()
new_channel = new channel channel_id
new_channel.updateProducts()
new_channel.getMeta().activeProducts = 2
expect(mockRestangular.one().one().get).toHaveBeenCalled
deferred.resolve({"products":[{"sku":"10413161","active":true,"min_price":{"fractional":412,"currency":"EUR"},"max_price":{"fractional":890,"currency":"EUR"}},{"sku":"10413162","active":true,"min_price":{"fractional":448,"currency":"EUR"},"max_price":{"fractional":799,"currency":"EUR"}}]}
)
$rootScope.$digest()
new_channel.updateProduct([{sku:"10413161",active:false,min_price:{fractional:400,currency:"EUR"},max_price:{fractional:950,currency:"EUR"}},{"sku":"10413162","active":true,"min_price":{"fractional":458,"currency":"EUR"},"max_price":{"fractional":799,"currency":"EUR"}}])
$httpBackend.flush()
expect(new_channel.getProducts()).toEqual(
[{"sku":"10413161","active":false,"min_price":{"fractional":400,"currency":"EUR"},"max_price":{"fractional":950,"currency":"EUR"}},{"sku":"10413162","active":true,"min_price":{"fractional":458,"currency":"EUR"},"max_price":{"fractional":799,"currency":"EUR"}}]
)
expect(new_channel.getMeta().activeProducts).toBe(1)
)
Because they are so long with all the objects in them I even start to put more "expects" into a single spec. I know that's wrong, but I am afraid of those huge specs.
Are there any best practices for structuring or refactoring Jasmine specs?

Use BeforeEach to put some initial common code of each spec, for instance, you could put those lines:
deferred = $q.defer()
spyOn(mockRestangular.one().one(), 'get').and.returnValue(deferred.promise)
spyOn(channel::, 'init').and.stub()
new_channel = new channel(channel_id)
in a BeforeEach, associated to the concerned describe.
beforeEach(function() {
deferred = $q.defer();
spyOn(mockRestangular.one().one(), 'get').and.returnValue(deferred.promise);
spyOn(channel::, 'init').and.stub();
new_channel = new channel(channel_id);
});
Other alternative: create some basic javascript function to gather common code.
The advantage would be that you could name those portions of code:
function mockDBGet() {
deferred = $q.defer();
spyOn(mockRestangular.one().one(), 'get').and.returnValue(deferred.promise);
}
function initChannel() {
spyOn(channel::, 'init').and.stub();
new_channel = new channel(channel_id);
}
//.......
it('myCurrentSpec', function(){
mockDBGet();
initChannel(); far more clean than your previous version
});

Related

Angular-Jasmine missing scopes in $httpbackend respond anonymous function

I am writing some end-to-end tests with Protractor for and Angular app. I am currently trying to mock some http responses using angular-mock and am running into a problem with scoping that I don't understand.
var protractor = require('protractor');
var ngMockE2E = require('ng-mock-e2e');
var testData = require('./e2e-data.json');
describe('DataEater', function() {
var $httpBackend = ngMockE2E.$httpBackend;
var appUrl = browser.baseUrl + 'scheduler/data-eater/';
var self = this;
self.testData2 = require('./e2e-data.json');
beforeEach(function() {
browser.get(appUrl);
ngMockE2E.addMockModule();
ngMockE2E.addAsDependencyForModule('dataEater');
$httpBackend.when('GET', '/scheduler/tasks/queue/')
.respond(function(method, url, data) {
console.log(testData);
console.log(self.testData2);
return [200, self.testData.history, {}];
});
Why are neither testData or testData2 defined? How can I get this data scoped properly so that I can return it as part of the response?
The problem might be that you need to explicitly pass the $scope to your controller. Try something like the following code:
beforeEach(
inject(function (_$controller_, _$rootScope_) {
myController = _$controller_('myControllerName', {
$scope: _$rootScope_.$new()
});
})
);

Creating a function in Angular

I have two controllers both with a save button which essentially does the same thing. So I want to put it in a reusable function that both the controllers can use. I have tried to do this by creating a normal function and passing the model object, as well as $http, but the function is executing before the save button is pressed leading to all the params being set to undefined. What way should I create a function that both these controllers can use?
Here how code looks:
app.controller('addCtlr',['$scope','$http','$location',
function($scope, $http, $location){
$scope.save = function(){
var practices = [];
var url = "https://maps.googleapis.com/maps/api/geocode/json?address="+$scope.location.address.replace(/ /g,"+");
//If there are practices
if($scope.days){
for(dayName in $scope.days){ //Loop through the days object
var day = $scope.days[dayName]; //Gets the day pratice object
practices.push({day: dayName, start_time: day.startTime, end_time: day.endTime}); //Add the pratice object to the practices array
}
}
//Call to get the lat lng and formatted address from Google Map's service
$http.get(url)
.then(function(response){
locJSON = response.data.results[0]; //The JSON response
//createing an object to send to the backend to save
var locObj = {
name: $scope.location.name,
address: locJSON.formatted_address,
location: locJSON.geometry.location,
cost: $scope.location.cost,
practices: practices,
notes: $scope.location.notes
};
//Sending using POST since a new object is being created
$http.post('/api/locations', locObj)
.then(
$location.path('/')
);
});//*/
};
}]);
This is how my function looked:
function saveLocation(location, days, $http){
var practices = [];
var url = "https://maps.googleapis.com/maps/api/geocode/json?address="+location.address.replace(/ /g,"+");
//If there are practices
if(days){
for(dayName in days){ //Loop through the days object
var day = days[dayName]; //Gets the day pratice object
practices.push({day: dayName, start_time: day.startTime, end_time: day.endTime}); //Add the pratice object to the practices array
}
}
//Call to get the lat lng and formatted address from Google Map's service
$http.get(url)
.then(function(response){
locJSON = response.data.results[0];
//createing an object to send to the backend to save
var locObj = {
name: location.name,
address: locJSON.formatted_address,
location: locJSON.geometry.location,
cost: location.cost,
practices: practices,
notes: location.notes
};
//Sending using POST since a new object is being created
$http.post('/api/locations', locObj)
.then(
//$location.path('/') //Redirects the user back to the homepage
);
});
}
This is how I was calling the function in the new controller:
app.controller('addCtlr',['$scope','$http','$location',
function($scope, $http, $location){
$scope.save = saveLocation(location, days, $http);
}]);
You can use service for this. Service is a singleton so will be created only one instance. And You can inject it by a dependency injector to controllers. You can read more here
You can create a service for your shared functionality and can inject it into your controller like below
var app=angular.module('app',[])
app.service('myService',function($http){
this.saveLocation=function(){
//Your code
}
});
and then in your controller you can inject it like below
app.controller('myController',['$scope','myService',function($scope,myService){
//use myService function to call save functionality
}]);
Also if you are using $http, you should keep this in mind that it returns a promise so you need to write all the code which is dependent on the value of this promise in a success callback otherwise your code will run before this callback and you will have undefined values for those variables.
Use a factory() service. You can define a set of functions and return them as an object. This object can then be injected within any controller:
app.factory('sharedFactory', [function() {
"use strict";
return {
myFunction: function() {
console.log("sharedFunction");
}
};
}]);
app.controller('AnyController', ['sharedFactory', function(sharedFactory) {
"use strict";
sharedFactory.myFunction();
}]);

Angular Cache $resource Result

I'm looking for a way to cache language translation in angular js application.
In the application, there are many form that need translation. To get the available languages, I use $resource to get them from our Language API.
First, I create an empty array of languages on application run()
angular
.module('admin', [])
.run(['$rootScope',
function($rootScope) {
$rootScope.languages = [];
}
)
;
Then I create a service to handle translation and language query
angular
.module('admin')
.factory('Utils', ['$rootScope', 'Language',
function($rootScope, Language) {
var utils = {};
utils.getLanguages = function() {
if ($rootScope.languages.length > 0) {
return $rootScope.languages;
}
var languages = Language.query(function(data) {
$rootScope.languages = data;
});
return languages;
}
return utils;
}
)
;
and in the controller
angular
.module('admin')
.controller('CategoryController', ['$scope', 'Utils',
function($scope, Utils) {
$scope.languages = Utils.getLanguages();
}
])
;
That's the way I cache the $resource result.
What do you think about this solution?
Is it ok to cache in $rootScope?
The reason I want to cache the result because I need the languages in most of the controller, so I don't want to make request for the Language API everytime I access a new state.
There as some improvement that you can do with your implementation.
You don't need to use $rootScope for saving language and then exposing it through a service. You can very well use a service and cache the results in the service.
Something like this should be better option
angular.module('admin')
.factory('LanguageCache', ['$rootScope', 'Language',
function ($rootScope, Language) {
var service = {};
var cache;
service.getLanguages = function () {
if (cache) {
return cache;
}
var languages = Language.query(function (data) {
cache = data;
});
return cache;
}
return service;
});
This way the language cache will be available for services that want it. It will not pollute the global $rootScope object.

Adding a Second Service to an Angular Module

Does anyone know if you can add a second service to an angular module?
It seems when I try, I end up breaking angular.
This Works:
var MyModule = angular.module('MyModule',[]);
MyModule.service('MyService',function(){
this.message = 'checking in';
});
MyModule.controller('MyController', function($scope,MyService){
console.log(MyService.message);
});
// output
// checking in
This DOES work: I had an issue elsewhere in my code. I'm leaving this up because it is still hard to find angular documentation with two services being applied (which I promise I looked for)
var MyModule = angular.module('MyModule',[]);
MyModule.service('MyService',function(){
this.message = 'checking in';
});
MyModule.service('MyServiceTwo',function(){
this.message = 'checking in';
});
MyModule.controller('MyController', function($scope,MyService,MyServiceTwo){
console.log(MyService.message);
});
// no output
In your second service definition just retrieve the module - don't redefine it:
var MyModule = angular.module('MyModule');

jasmine, $q is undefined

I'm going through some of the examples online for AngularJS to try to understand how it works. I'm trying to use jasmine to test like in the examples. In my spec file, I have:
var Person = function (name, $log) {
this.eat = function (food) {
$log.info(name + " is eating delicious " + food);
};
this.beHungry = function (reason) {
$log.warn(name + " hungry " + reason);
};
};
var bob = new Person();
describe("describe", function () {
it("$q", function () {
var pizzaOrderFulfillment = $q.defer();
var pizzaDelivered = pizzaOrderFulfillment.promise;
pizzaDelivered.then(bob.eat, bob.beHungry);
pizzaOrderFulfillment.resolve("resolved");
$rootScope.$digest();
expect($log.TypeInfo.logs).toContain(["resolved"]);
});
});
I get
ReferenceError: $q is not defined
Am I using Jasmine correctly? I basically am just writing all my angular and jasmine code in the spec.js file. When I had the angular code in another file, my spec.js file couldn't find it. Probably because I didn't set any dependencies up on what gets loaded first since I'm just starting out with this stuff.
Edit, fixed the $ to be $q and the referencerror.
I guess you are not injecting the $q service in your unit test.
For example in your beforeEach block you can inject it:
var q;
beforeEach(inject(function($q) {
q = $q;
}));
And then in your unit test:
describe("describe", function () {
it("$q", function () {
var pizzaOrderFulfillment = q.defer();
var pizzaDelivered = pizzaOrderFulfillment.promise;
pizzaDelivered.then(bob.eat, bob.beHungry);
pizzaOrderFulfillment.resolve("resolved");
$rootScope.$digest();
expect($log.TypeInfo.logs).toContain(["resolved"]);
});
});

Categories

Resources