I am using this strategy to lazy-load stuff with RequireJS in my AngularJS app:
define([
'src/services/dependency_resolver', // resolves promise when dependencies are `require`d
'json!modules.json'
], function (dependencyResolver, modules) {
var app = angular.module('myApp', [ 'ngRoute' ]);
app.config(function ($controllerProvider, $routeProvider) {
app.lazy = {
controller: $controllerProvider.register
// <...> other providers
};
angular.forEach(modules, function (moduleConfig) {
angular.forEach(moduleConfig.routes, function (route) {
$routeProvider.when(route.path, {
templateUrl: route.templateUrl,
controller: route.controller,
resolve: dependencyResolver(moduleConfig.dependencies)
});
});
});
});
return app;
});
But I'm not sure what is the correct way test a lazy-loaded controller. It is registered like this:
define(['src/app'], function (app) {
app.lazy.controller('MainCtrl', function () {
//
});
});
And this is my current spec:
describe('`MainCtrl` controller', function () {
var Ctrl,
$scope;
beforeEach(angular.mock.module('myApp'));
beforeEach(function (done) {
require(['module/main'], done);
});
beforeEach(function () {
angular.mock.inject(function ($rootScope, $controller) {
$scope = $rootScope.$new();
Ctrl = $controller('MainCtrl', {
$scope: $scope
});
});
});
it('should ...', function () {
console.log(Ctrl);
});
});
With this spec, an error occurs when controller is being registered, because app.lazy is undefined.
So the question is how to test such controllers?
Cheers!
I was experiencing a similar problem when writing my unit test using the "lazy" property to register my controller. The problem with this approach is that when in the context of a unit test, the module config block will not be executed and as a result, app.lazy will resolve to undefined.
To solve your problem, instead of using provider registration methods to set your properties of app.lazy, the provider registration method should be used to override their counterparts on the module. In other words, your config block should now become:
`app.config(function ($controllerProvider, $routeProvider) {
app.controller = $controllerProvider.register
// <...> other providers
.......
}`
Instead of register your controller using (app.lazy):
`define(['src/app'], function (app) {
app.lazy.controller('MainCtrl', function () {
//
});
});`
you can just define like this:
`define(['src/app'], function (app) {
app.controller('MainCtrl', function () {
//
});
});`
And this should work! Hopefully this can help, and please let me know if this works out or not.
First of all, thank you for the reference you provided - the article is really interesting.
Author of the article is using AngularJs providers to implement his strategy. The thing is, that AngularJs doesn't have providers for 'specs'. So my opinion is that you should omit this strategy in your unit tests.
On this basis, I think, that you should add AMD to your spec file. Define your controller as a dependency in your spec. After this, you may just require all your specs somewhere in main-spec.js and launch your testing framework.
Related
I have a constant which is injected into a controller and I need to write a test which changes this constant and expects different results. I can use $provide to mock the constant but according to articles I've found online, I need to do it in the module declaration, which I believe is like this:
beforeEach(module("someModule"));
beforeEach(function () {
module(function ($provide) {
$provide.constant('someConstant', false);
});
});
I later load the controller like this:
function createController() {
view = $controller(
"someController",
{
$scope: $injector.get("$rootScope").$new()
});
}
Where $controller, $scope and $injector are all injected in my main beforeEach
This does provide the constant and it does change if I change the value in my beforeEach. But only for the entire test suite. I want to change this constant in a describe or an it but I'm not sure how. If I move the $provide down to a describe or it, I get the error:
Error: Injector already created, can not register a module!
I could just create a new file and that is probably what I'm going to do but is there a way I can $provide a dynamic value?
Lets consider such a code
controller
angular.module('someModule', [])
.controller('someController', function($scope, someConstant) {
$scope.someProvidedValue = someConstant;
})
and test for it
controller spec
describe('module', function () {
beforeEach(module("someModule"));
var createController;
beforeEach(inject(function (_$controller_, _$injector_) {
scope = _$injector_.get("$rootScope").$new()
createController = function createController(scope, obj) {
_$controller_("someController", {
$scope: scope,
someConstant: obj.someConstant
});
}
}))
it('someConstant', function () {
expect(scope.someProvidedValue).toBe(undefined)
createController(scope, {
someConstant: false
})
expect(scope.someProvidedValue).toBe(false)
})
it('someConstant', function () {
expect(scope.someProvidedValue).toBe(undefined)
createController(scope, {
someConstant: true
})
expect(scope.someProvidedValue).toBe(true)
})
})
In the mean time I'm looking for looks nicer solution.
I want to unit test my controller. I started with basic test assertions of expect API. But I am facing challenge in mocking scope methods inside a conditional check. I am getting an undefined error since it is not available under scope, only the global logout() method is available.
I tried mocking the localStorageService using spyOn as true to satisfy the condition, but that's still of no help. Any solution will be of great help to get me kickstarted.
Controller:
angular.module('app').controller('sampleCtrl',
function($scope, $state, $http, $rootScope, localStorageService) {
if (!(localStorageService.get('isAuthenticated'))) {
$state.go('home');
}
if (localStorageService.get('isAuthenticated') === true) {
//http post calls made here to perform certain operation on page load
$scope.someMethod = function(){
//do something
}
}
$scope.logOut = function() {
localStorageService.set('property', '');
localStorageService.set('isAuthenticated', false);
$state.go('home');
};
});
Karma:
'use strict';
describe('Controller: sampleCtrl', function() {
/** to load the controller's module */
beforeEach(module('app'));
var sampleCtrl,scope,httpBackend,deferred,rootScope;
beforeEach(inject(function ($controller,_$rootScope_,$httpBackend,$q) {
var store = {};
scope= _$rootScope_.$new(); // creates a new child scope of $rootScope for each test case
rootScope = _$rootScope_;
localStorageService = _localStorageService_;
httpBackend = $httpBackend;
httpBackend.whenGET(/\.html$/).respond('');
spyOn(localStorageService, 'set').and.callFake(function (key,val) {
store[key]=val;
});
spyOn(localStorageService, 'get').and.callFake(function(key) {
return store[key];
});
sampleCtrl = $controller('sampleCtrl',{
_$rootScope_:rootScope,
$scope:scope,
$httpBackend:httpBackend,
_localStorageService_:localStorageService
// add mocks here
});
localStorageService.set('isAuthenticated',true);
}));
/**ensures $httpBackend doesn’t have any outstanding expectations or requests after each test*/
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
httpBackend.verifyNoOutstandingRequest();
});
it('sampleCtrl to be defined:',function(){
httpBackend.flush();
expect(sampleCtrl).toBeDefined();
});
// failing test case - scope.someMethod not available in scope
it('is to ensure only authenticated user can access the state methods',function(){
localStorageService.get('isAuthenticated');
httpBackend.flush();
expect(scope.someMethod).toBeDefined();
});
});
I've managed to get it work.
The problem was that localStorageService did not have isAuthenticated set to true on starting the controller. Place setting it to true before calling the controller.
In my angular app, I have around 30 controllers and 1 test file for each of them. Those test files always begin with something like this boilerplate:
'use strict';
describe('Controller: SomeController', function ()
{
var controller;
var scope;
beforeEach(function ()
{
module('SomeControllerModule');
inject(function ($controller, $rootScope)
{
scope = $rootScope.$new();
controller = $controller('SomeController',
{
$scope: scope
});
});
});
it('should prepare controller scope', function ()
{
console.log('scope', scope);
});
});
Is there a way to somehow make it shorter, so that I don't have to repeat it in each of my files?
There's ng-describe which looks like it could be very useful (I haven't used it personally yet). With that your code becomes something like:
ngDescribe({
modules: 'SomeControllerModule',
controllers: 'controller',
tests: function (deps) {
it('should prepare controller scope', function () {
console.log('scope', deps.controller);
});
}
});
Unfortunately they don't support jasmine at the moment which probably rules it out for a lot of people.
Yes, I have a cleaner way of doing it:
describe('HomeController', function() {
var $scope;
beforeEach(module('app'));
beforeEach(inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
$controller('HomeController', { $scope: $scope });
}));
it('true should be truthy', function() {
expect(true).toBeTruthy();
});
});
Also check out this AngularJS Scaffolding that comes with all nuts and bolts you might need in your AngularJS project.
This post was based on this.
My intention is to separate components on a file basis. For example, I want a specific controller to have it's own file (Same goes with services, filters and directives). Of course, files will be group together based on the module they will fall into. Here's an overview of what I currently have:
Directory
User/
User/UserModule.js
User/UserDirective.js
User/UserService.js
User/UserFilter.js
User/UserController.js
UserModules.js
UserModule = angular.module('UserModule', []);
UserModule.controller('userCtrl', ['$scope', 'UserService', UserCtrl])
.factory('userService', function() {
return new UserService();
})
.filter('userFilter', UserFilter)
.directive('userDirective', UserDirective);
UserController.js
UserCtrl = function($scope, UserService) {
// ...
};
UserDirective.js
UserDirective = function() {
return {
// ...
}
};
UserService.js
UserService = function() {
// ...
};
UserFilter.js
UserFilter = function() {
return function() {
// ...
}
};
Then I'll just push the user module to the app module.
app.requires.push('UserModule');
My concern lies on the registration of the concepts (Such as controllers, services...) to the module. I was wondering if this is the best way to go and if it's correct. Also possible issues on the parameters and the external js file.
Consider this part:
.controller('userCtrl', ['$scope', 'UserService', UserCtrl])
The UserCtrl above refers to a function defined in a separate file. Will I be able to pass the $scope and UserService dependency as parameters to the UserCtrl?
UserCtrl = function($scope, UserService) { // Pass parameters (UserController.js)
What's the correct way of doing this in terms of Services, Filters and Directives?
Finally, how can I improve the code?
I'm also using Meteor btw.
You do not need to declare global variables UserModule, UserDirective, UserService. You just need to declare/register them as below. Angular will take care of injecting dependencies.
UserModule.js
angular.module('UserModule', []);
UserDirective.js
angular.module('UserModule').directive('userDirective', function() {
return {
// ...
}
});
UserService.js
angular.module('UserModule').service('UserService', function() {
});
UserController.js
angular.module('UserModule').controller('UserController', ['$scope', 'UserService', function($scope, UserService){
}])
I've seen similar questions asked, but not one of them resolves the root of the problem, which assumes the following:
I can't use whenGET('').passThrough(), because I'm not using ngMockE2E.
I shouldn't need to use ngMockE2E because I'm writing a unit test for a directive that literally does nothing but spit out "bar".
One suggestion was to use a proxy server to serve-up these HTTP responses, but doesn't that defeat the purpose of a unit test?
Now, let me show you my directive:
angular.module('app')
.directive('foo', function () {
return {
restrict: 'A',
templateUrl: 'templates/bar.html'
};
});
And here is the unit test:
describe('Directive: foo', function () {
// load the directive's module
beforeEach(module('app'));
var $compile;
var $rootScope;
var $httpBackend;
beforeEach(inject(function(_$compile_, _$rootScope_, $injector) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$httpBackend = $injector.get('$httpBackend');
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should be bar', inject(function($templateCache) {
$templateCache.put('templates/bar.html', 'bar');
var element = $compile('<div data-foo></div>')($rootScope);
//$httpBackend.whenGET(/^\/translations\//).passThrough(); // doesn't work
$httpBackend.expectGET(/\/translations\//).respond('201', ''); // works
$rootScope.$digest();
$httpBackend.flush();
expect(element.text()).toBe('bar'); // works
}));
});
Now, the test works just fine with all this bogus $httpBackend business in there, but this totally doesn't belong in my unit tests! How do I pull this $httpBackend crap out and stop the AngularJS app config blocks from running in my directive unit tests with a templateUrl?
Note: This only happens when there is a templateUrl in the directive.
BTW, the code that's triggering this HTTP request in the app config block is:
$translatePartialLoaderProvider.addPart('index');
$translateProvider.useLoader('$translatePartialLoader', {
urlTemplate: '/translations/{part}/{lang}.json'
});
But I don't think that's necessarily relevant.
Put your directives into their own module and include that module into your main app module. Then you can test your directive module without loading up your app module.
e.g.
angular.module('app-directives', [])
.directive('foo', function () {
return {
restrict: 'A',
templateUrl: 'templates/bar.html'
};
});
angular.module('app', ['app-directives'])
//Run your config stuff here