I am trying BDD with Jasmine and angularJS app. My requirement is to create a select element in my view which gets it's data from a factory. So in true spirit of BDD, I am writing my factory and controller first before I start writing my view.
My test for factory:
describe('getTypeOfUnit', function(){
it('should return typeofunits', inject(function(getTypeOfUnit){
expect(getTypeOfUnit).not.toBeNull();
expect(getTypeOfUnit instanceof Array).toBeTruthy();
expect(getTypeOfUnit.length).toBeGreaterThan(0);
})) ;
});
So I am testing that my data is not null, is an array and contains at least one item. It fails since there is no factory.
This is the factory to make the tests pass:
angular.module('myApp.services', [])
.factory('getTypeOfUnit', function(){
var factory = ['Research Lab', 'Acedamic Unit', 'Misc'];
return factory;
});
Now onto the controller. Here is the empty controller:
angular.module('myApp.controllers', [])
.controller('describeUnitController',[function($scope){
console.log('exiting describeUnit');
}]);
And tests for controller:
describe('controllers', function(){
var describeScope;
beforeEach(function(){
module('myApp.controllers');
inject(function($rootScope, $controller){
console.log('injecting contoller and rootscope in beforeEach');
describeScope = $rootScope.$new();
var describerController = $controller('describeUnitController', {$scope: describeScope});
});
}) ;
it('should create non empty "typeOfUnitsModel"', function() {
expect(describeScope["typeOfUnits"]).toBeDefined();
var typeOfUnits = describeScope.typeOfUnits;
expect(typeOfUnits instanceof Array).toBeTruthy();
expect(typeOfUnits.length).toBeGreaterThan(0);
});
});
So I am testing that my controller returns a non empty array. Same as the service. These tests fail. So next step is to define a property on the scope object in the controller:
.controller('describeUnitController',[function($scope){
$scope.typeOfUnits = [];
console.log('exiting describeUnit');
}]);
Now I get an error:
TypeError: Cannot set property 'typeOfUnits' of undefined
Why does the controller not know about the scope? I thought DI would automatically make it available?
Thank you in advance. Also please comment on my tests.
Found two mistakes with my code:
The controller does not know about the $scope. Not sure why. So I can do one of the following:
.controller('describeUnitController',['$scope',function($scope){
$scope.typeOfUnits = [];
console.log('exiting describeUnit');
}]);
OR
.controller('describeUnitController',function describeUnitController($scope){
$scope.typeOfUnits = [];
console.log('exiting describeUnit');
});
I am not sure why it is this way. But lesson learned.
I then tried to use the service as follows:
.controller('describeUnitController',function describeUnitController($scope, GetTypeOfUnit){
$scope.typeOfUnits = getTypeOfUnit;
});
This gave me the famous Error: unknown provider for GetTypeOfUnit
Apparantly, I have to add the services module to the factory module in order to use the service and make the tests pass. But my app.js has them all defined:
angular.module('myApp', ['myApp.filters', 'myApp.services', 'myApp.directives', 'myApp.controllers','ui.bootstrap','$strap.directives'])
But since I am testing, I have to load the services module in the controllers module as well. If the app was loading (like in the browser), I would not have to do this.
Am I understanding this correctly? Thank you all.
Related
I am working on AngularJs 1.5 and just started off with Jasmine and Karma for testcases...
My controllers make use of
locale.ready('common').then(function () {
})
I am not able to mock 'locale'. Though its should have been straightforward as for other services.
I found something on the internet for this purpose, but the documentation does not have a working code:
'karma-json-preprocessor'
Can I get a sample code that demonstrates how to mock locale in this scenario?
EDIT: (29May'17)
Below is the sample code:
promise returns the object containing the localization keys & values:
angular.module('myApp', [])
.controller('mytestcontroller', ['$scope', 'locale',
function ($scope, locale) {
locale.ready('common').then(function (res) {
$scope.reportname = 'gauravreport';
});
}
]);
Below is a sample code I am trying:
var scope, controller;
beforeEach(inject(function($controller, $rootScope, locale) {
scope = $rootScope.$new();
controller = $controller('mytestcontroller', {
'$scope': scope,
'locale': locale
});
}));
it('check report name', function(done) {
var data = window.$json.$get('app/languages/en-US/home.lang.json');
locale.ready('myreport')
.then(function() {
expect(scope.reportname).toBe("gauravreport");
done();
})
.catch(done.fail);
});
Issue is when I run this test, it says reportname is not defined. It seems that its not able to resolve the locale service.
I have resolved this issue, using the approach stated here:
Testing Promises with Jasmine - Provide and Spy
I have a factory that needs to listen for a broadcast event. I injected $scope into the factory so I could use $scope.$on. But as soon as I add $scope to the parameter list I get an injector error.
This works fine:
angular.module('MyWebApp.services')
.factory('ValidationMatrixFactory', ['$rootScope', function($rootScope) {
var ValidationMatrixFactory = {};
return ValidationMatrixFactory;
}]);
This throws an injector error:
angular.module('MyWebApp.services')
.factory('ValidationMatrixFactory', ['$scope', '$rootScope', function($scope, $rootScope) {
var ValidationMatrixFactory = {};
return ValidationMatrixFactory;
}]);
Why can't I inject $scope into a factory? And if I can't, do I have any way of listening for events other than using $rootScope?
Because $scope is used for connecting controllers to view, factories are not really meant to use $scope.
How ever you can broadcast to rootScope.
$rootScope.$on()
Even though you can't use $scope in services, you can use the service as a 'store'. I use the following approach inspired on AltJS / Redux while developing apps on ReactJS.
I have a Controller with a scope which the view is bound to. That controller has a $scope.state variable that gets its value from a Service which has this.state = {}. The service is the only component "allowed" (by you, the developer, this a rule we should follow ourselves) to touch the 'state'.
An example could make this point a bit more clear
(function () {
'use strict';
angular.module('app', ['app.accounts']);
// my module...
// it can be defined in a separate file like `app.accounts.module.js`
angular.module('app.accounts', []);
angular.module('app.accounts')
.service('AccountsSrv', [function () {
var self = this;
self.state = {
user: false
};
self.getAccountInfo = function(){
var userData = {name: 'John'}; // here you can get the user data from an endpoint
self.state.user = userData; // update the state once you got the data
};
}]);
// my controller, bound to the state of the service
// it can be defined in a separate file like `app.accounts.controller.js`
angular.module('app.accounts')
.controller('AccountsCtrl', ['$scope', 'AccountsSrv', function ($scope, AccountsSrv) {
$scope.state = AccountsSrv.state;
$scope.getAccountInfo = function(){
// ... do some logic here
// ... and then call the service which will
AccountsSrv.getAccountInfo();
}
}]);
})();
<script src="https://code.angularjs.org/1.3.15/angular.min.js"></script>
<div ng-app="app">
<div ng-controller="AccountsCtrl">
Username: {{state.user.name ? state.user.name : 'user info not available yet. Click below...'}}<br/><br/>
Get account info
</div>
</div>
The benefit of this approach is you don't have to set $watch or $on on multiple places, or tediously call $scope.$apply(function(){ /* update state here */ }) every time you need to update the controller's state. Also, you can have multiple controllers talk to services, since the relationship between components and services is one controller can talk to one or many services, the decision is yours. This approach focus on keeping a single source of truth.
I've used this approach on large scale apps... it has worked like a charm.
I hope it helps clarify a bit about where to keep the state and how to update it.
I am having a hard time understanding unit tests in angularJs. I have just started with unit tests and the syntax seems weird to me. Below is the code for testing a controller :
describe('PhoneCat controllers', function() {
describe('PhoneListCtrl', function(){
beforeEach(module('phonecatApp'));
it('should create "phones" model with 3 phones',
inject(function($controller) {
var scope = {},
ctrl = $controller('PhoneListCtrl', {$scope:scope});
expect(scope.phones.length).toBe(3);
}));
});
});
What I can understand from this syntax is that before each it block phonecatApp is initialised and that $controller service is used to get an instance of PhoneListCtrl controller.
However I am not able to understand the scope thing here. Can someone elaborate on whats behind getting the scope of the controller on this line.
ctrl = $controller('PhoneListCtrl', {$scope:scope});
Normally, at runtime, angular creates a scope and injects it into the controller function to instantiate it.
In your unit test, you instead want to create the scope by yourself and pass it to the controller function, in order to be able to see if it indeed has 3 phones after construction (for example).
You might also want to inject mock services instead of the real ones into your controller. That's what the array of objects allows in
$controller('PhoneListCtrl', {$scope:scope});
It tells angular: create an instance of the controller named 'PhoneListCtrl', but instead of creating and injecting a scope, use the one I give you.
If your controller depended on a service 'phoneService', and you wanted to inject a mock phoneService, you could do
var mockPhoneService = ...;
$controller('PhoneListCtrl', {
$scope: scope,
phoneService: mockPhoneService
});
It's not necessary to inject the scope, you can directly use the instance of controller to call the controller's functions and objects.In your example you can use like below, this will give the same result set as yours
describe('PhoneCat controllers', function() {
describe('PhoneListCtrl', function(){
beforeEach(module('phonecatApp'));
it('should create "phones" model with 3 phones',
inject(function($controller) {
var ctrl = $controller('PhoneListCtrl');
expect(ctrl.phones.length).toBe(3);
}));
});
});
and for you information each time the controller is instantiated it is bound to a $scope variable which is derived from $rootScope (i.e: child of rootscope). So you need to pass the $scope to grab the instance of controller and I am doing same thing in above example.
I'm trying to call a web service in AngularJS bootstrap method such that when my controller is finally executed, it has the necessary information to bring up the correct page. The problem with the code below is that of course $rootScope is not defined in my $http.post(..).then(...
My response is coming back with the data I want and the MultiHome Controller would work if $rootScope were set at the point. How can I access $rootScope in my angular document ready method or is there a better way to do this?
angular.module('baseApp')
.controller('MultihomeController', MultihomeController);
function MultihomeController($state, $rootScope) {
if ($rootScope.codeCampType === 'svcc') {
$state.transitionTo('svcc.home');
} else if ($rootScope.codeCampType === 'conf') {
$state.transitionTo('conf.home');
} else if ($rootScope.codeCampType === 'angu') {
$state.transitionTo('angu.home');
}
}
MultihomeController.$inject = ['$state', '$rootScope'];
angular.element(document).ready(function () {
var initInjector = angular.injector(["ng"]);
var $http = initInjector.get("$http");
$http.post('/rpc/Account/IsLoggedIn').then(function (response) {
$rootScope.codeCampType = response.data
angular.bootstrap(document, ['baseApp']);
}, function (errorResponse) {
// Handle error case
});
});
$scope (and $rootScope for that matter) is suppose to act as the glue between your controllers and views. I wouldn't use it to store application type information such as user, identity or security. For that I'd use the constant method or a factory (if you need to encapsulate more logic).
Example using constant:
var app = angular.module('myApp',[]);
app.controller('MainCtrl', ['$scope','user',
function ($scope, user) {
$scope.user = user;
}]);
angular.element(document).ready(function () {
var user = {};
user.codeCampType = "svcc";
app.constant('user', user);
angular.bootstrap(document, ['myApp']);
});
Note Because we're bootstrapping the app, you'll need to get rid of the ng-app directive on your view.
Here's a working fiddle
You could set it in a run() block that will get executed during bootstrapping:
baseApp.run(function($rootScope) {
$rootScope.codeCampType = response.data;
});
angular.bootstrap(document, ['baseApp']);
I don't think you can use the injector because the scope isn't created before bootstrapping. A config() block might work as well that would let you inject the data where you needed it.
I'm currently trying to write tests for existing blocks of code and running into an issue with a controller that has a nested ng-grid inside of it. The issue comes from the controller trying to interact with the grid on initialization.
Testing Software
node#0.10.14
karma#0.10.2
karma-jasmine#0.1.5
karma-chrome-launcher#0.1.2
My Test:
define(["angularjs", "angular-mocks", "jquery",
"js/3.0/report.app",
"js/3.0/report.controller",
"js/3.0/report.columns"
],
function(angular, ngMocks, jquery, oARModule, oARCtrl, ARColumns) {
"use strict";
describe("Report Center Unit Tests", function() {
var oModule;
beforeEach(function() {
oModule = angular.module("advertiser_report");
module("advertiser_report");
});
it("Advertiser Report Module should be registered", function() {
expect(oModule).not.toBeNull();
});
describe("Advertiser Report Controller", function() {
var oCtrl, scope;
beforeEach(inject(function($rootScope, $controller, $compile) {
var el = document.createElement('div');
el.setAttribute('ng-grid','gridOptions');
el.className = 'gridStyle';
scope = $rootScope.$new();
$compile(el)(scope);
oCtrl = $controller('ARController', {
$scope: scope
});
}));
it("Advertiser Report controller should be registered", function() {
expect(oCtrl).not.toBeNull();
});
});
});
});
You'll see where I've tried to create and compile an element with the ng-grid attribute. Without doing this I get the following error:
TypeError: Cannot read property 'columns' of undefined
Which is a result of the controller attempting to call things like
$scope.gridOptions.$gridScope.columns.each
So I added the creation of a div with ng-grid attribute, and got a new error:
TypeError: Cannot set property 'gridDim' of undefined
So, I tried to add scope.gridOptions before the $controller call, but this brought me back to the original error. I've been searching for way to make this work without rewriting the controller and/or templates, since they are currently working correctly in production.
Your (major!) problem here is that the controller is making assumptions about a View. It should not know about and thus not interact with ng-grid. Controllers should be View-independent! That quality (and Dependency Injection) is what makes controllers highly testable. The controller should only change the ViewModel (i.e. its $scope), and in testing you validate that the ViewModel is correct.
Doing otherwise goes against the MVVM paradigm and best practices.
If you feel like you must access the View (i.e. directives, DOM elements, etc...) from the controller, you are likely doing something wrong.
The problem in the second Failing test is gridOptions and myData is not defined prior to the compilation. Notice the sequence of the 2 statements.
Passing
oCtrl = $controller('MainCtrl', { $scope: $scope });
$compile(elm)($scope);
Failing
$compile(elm)($scope);
oCtrl = $controller('MainCtrl', { $scope: $scope });
In both cases you are trying to use the same html
elm = angular.element('<div ng-grid="gridOptions" style="width: 1000px; height: 1000px"></div>');
I suggest you get rid of
oCtrl = $controller('MainCtrl', { $scope: $scope });
maneuvers and use the following HTML element instead
elm = angular.element('<div ng-controller="MainCtrl"
ng-grid="gridOptions" style="width: 1000px; height: 1000px"></div>');
Notice ng-controller="MainCtrl".
So the end story is that you need gridOptions defined somewhere so
that it ngGrid can access it. And make sure gridOptions dependent
code in controller is deferred in a $timeout.
Also take a look at the slight changes in app.js
$timeout(function(){
//your gridOptions dependent code
$scope.gridOptions.$gridScope.columns.each(function(){
return;
});
});
Here is the working plnkr.