angular auto mocking controller/service dependencies - angularjs

In Angular unit testing, it takes a lot of unnecessary code to mock/stub all depencencies manually, especially when all I need are some generic mocks. Also, when dependency list changes for service/controller, tests break because of missing dependencies.
In C#, there is a way to reconfigure DI container to automatically mock all dependencies, when Resolve() is called. I want something like that in Angular.
I want to go from this:
beforeEach(inject(function ($controller, $rootScope, agsRest) {
scope = $rootScope.$new();
sut = $controller('SearchController', {
$scope: scope
, map: {}
, agsRest: agsRest
, config: {}
, core: {}
, myPopup: {}
, popupFormatter: {}
});
}));
To something like this:
beforeEach(inject(function ($controller, $rootScope, agsRest) {
scope = $rootScope.$new();
sut = autoMock("SearchController"); // instance of SearchController is returned, which has all dependencies mocked with sinon/jasmine/whatever
}));
Is there some kind of library / code to do this?

I've needed something similar and came up with this small utility library: tentacle.js. I'm up to suggestions and to pull requests for bettering the API.

You should look for ng-improved-testing. It's create auto-mock for all injected services (you need only add "Mock" to services name when you inject them).
Link to GitHub:
https://github.com/evangalen/ng-improved-testing

Related

Use $componentController in production?

I am building a dialog service. A dialog can have a controller, very similar to $mdDialog, like this:
myDialogService.show({
templateUrl: `<div ng-click="$ctrl.log()">Hello dialog</div>`,
controller: function() {
this.log = function() {
console.log("logged from myDialogController");
}
}
});
which works great. I invoke the controller that way:
locals.$scope = scope;
const invokeController = $controller(options.controller, locals, true);
const controller = invokeController();
if (options.controllerAs) {
scope[options.controllerAs] = controller;
} else {
const controllerAs = "$ctrl";
scope[controllerAs] = controller;
}
In angular-mock is the $componentController service, which can invoke component controllers. With my code, I only can invoke registered controllers, or given controller functions. This is not very helpful, as I only have components registered, not single controllers.
My question
Is it possible/recommended to use the $componentController in production? Or is there any AngularJS build in variant I have overseen?
$componentController belongs to ngMock module because it is useful for testing but is considered a hack in production. Since ngMock is big enough and isn't supposed to be available in production, it should be pasted in order to become available.
The proper way to solve this is to either have registered controllers that are reused as component controllers or import/export controller functions/classes with JS modules.
Since MdDialogController belongs to third-party module, isn't registered or exported and is small, it can be just pasted.

How should I access angular services outside of DI lifecycles ($rootScope in particular)?

The short story is that I'm trying to lever some data model code that's not written for angular in particular into an angular application. This code is written using ES6 import / export syntax for modules, and I'd like to keep using that. So I have something like:
export class DataModel {
//some stuff with promises
}
What I did was create a utility module that exposes the relevant Angular (1.5) services to the ES6 module system thusly:
import angular from 'angular';
export const services = {};
angular.injector(['ng', 'toastr']).invoke([
'$q',
'$http',
'$rootScope',
(
$q,
$http,
$rootScope
) => {
services.$q = $q;
services.$http = $http;
services.$rootScope = $rootScope;
},
]);
Then I can just import the $q library into my DataModel classes and hey presto, everything kind of works - I'm doing promises, and the appropriate scopes should update when the .then methods fire.
The problem is that this doesn't actually work. I'm 90% sure that the reason this doesn't work is that the $rootScope element I get from the angular.injector call isn't a singleton rootscope, it's a fresh new one that gets created just for this context. It does not share any scope linkage with the actual scope on the page (I can confirm this by selecting a DOM element and comparing services.$rootScope to angular.element($0).scope().$root). Therefore, when a promise resolves or a $http returns, I get the data but have the standard symptoms of not notifying a scope digest in the interface (nothing changes until I manually trigger a digest).
All I really want is a copy of the $q, $rootScope and $http services that angular uses live in the active page. Any suggestions are welcome. My next try will be to see if I can grab the relevant services from some .run block where I inject $q et al instead of doing it with the injector. That introduces some problematic timing issues, though, since I need to bootstrap angular, run the run block, and then expose the services to my data model. But the bootstrapping process requires the datamodel. It's a bit circular.
I'm answering this myself for now, but would love to see any other ideas.
I changed the angularServices code to look like:
import angular from 'angular';
import { Rx } from 'rx-lite';
export const servicesLoaded = new Rx.Subject();
export const services = {};
angular.module('app.services', []).run([
'$q',
'$http',
'$rootScope',
(
$q,
$http,
$rootScope
) => {
services.$q = $q;
services.$http = $http;
services.$rootScope = $rootScope;
servicesLoaded.onCompleted();
},
]);
Since I was already using rx-lite anyway. This allows me to do
import { services } from 'angularServices';
services.$http(options) // etc;
whenever I'm working in code that is run after the application bootstrap cycle. For the code that was running prematurely (it was just config stuff that was in a few places, I wrapped it inside the RxJS event thusly:
import { services, servicesLoaded } from '../../common/angularServices';
servicesLoaded.subscribeOnCompleted(() => {
services.$rootScope.$on('$stateChangeSuccess', () => {
//etc
That way I don't try to get in touch with $rootScope or $window before it actually exists, but the $q, $rootScope, and $http I've stashed a reference to in my services object is actually a real thing, and digests all fire properly.
And now hey presto, while my model layer references $http and $q, they'll be pretty easy to exchange with some other provider of promises and XHRs, making all the work I put into that not bound to angular 1.x. Whee.

Angular JS Test driven Development with multiple controllers

I have developed an web application using Angular JS. I am getting few additional CR which needs to implemented in using TTD approach. We have return unit test cases using Jasmine and Karma. The challenge currently we face is when we try to write unit test case for multiple controllers. I have a main page return on Home Controller & its has an broadcast event in another controller. When i write a unit test case Object for the controller which has this broadcast event is not initialized.
Is there any way to inject the second controller as a dependent object. Answers with Reference Sample link or demo code is much appreciated.
You state you are using Jasmine and Karma so I assume you are unit testing. If you are "unit" testing you should test each controller individually while mocking, spy, all injected services.
beforeEach(inject(function ($rootScope, $controller) {
rootScope = $rootScope;
scope = $rootScope.$new();
controller = $controller('MyCtrl as ctrl', {
'$scope': scope
});
}));
it('', function(){
//Arrange
controller.counter = 0; // Your controller is listening on scope.$on to update this counter.
//Act
rootScope.$broadcast('xyz', {});
//Assert
expect(controller.counter == 1).toBe(true);
rootScope.$broadcast('xyz', {});
expect(controller.counter == 2).toBe(true);
rootScope.$broadcast('xyz', {});
expect(controller.counter == 3).toBe(true);
});
Just be careful with broadcast. Only a domain events (model updated/deleted/created), or something global (signin,signout) should travel over $broadcast. Otherwise, it should be replaced with a service + directive. An example is angular material https://material.angularjs.org/latest/api/service/$mdDialog that is 1 directive with a backing service that can be open/closed from anywhere.
You can inject any controller with the $controller service, e.g.
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
controller = $controller('MyCtrl', {
'$scope': scope
});
}));
see docs here:
https://docs.angularjs.org/api/ngMock/service/$controller
so what you do is inject that controller first, then your other controller. then the first controller will have been instantiated at the time the second gets instantiated.
I am new to angular, it seems to be possible to inject multiple controllers at once. However best practice is to generate a mock controller that behaves as you expect the second controller to behave. This reduces the number of things you are testing at once.
The following link may be helpful for creating a mock controller, http://www.powdertothepeople.tv/2014/08/28/Mocking-Controller-Instantiation-In-AngularJS-Unit-Test/ .

Controller testing fails due to the service dependency injection

When making use of a service in a controller test do you need to initialize the service in the same way you would the controller? By this I mean do you need to pass it its own dependencies?
For example I can initialize my controller like so:
// Instantiate the controller
searchController = $controller( 'VisibilitySearchController',{
$scope: scope,
dataService: dataService
});
}));
so do I need to initialize the service according to the components it needs like $http, $resource etc as well as make spyOn calls on its functions? Or is this/should this be sufficient? (Note - my tests fail when I do the following )
// Instantiate the dataService
dataService = $injector.get( 'dataService' );
it throws this error:
* Error: [$injector:unpr] Unknown provider: $resourceProvider <- $resource <- dataService
The relevant part of the service:
myAppServices.factory('dataService', ['$http', '$resource', 'authService', 'messageService', function ($http, $resource, authService, messageService) {
}
Side note
Note - we are using Maven as our build tool and only make use of Jasmine at this point - trying to bring Karma in as our test-runner as a Maven plugin.
You must provide all the dependencies but you can mock them. This can be done by jasmine like this for example:
var mockedDataService = jasmine.createSpyObj('dataService', ['getData', 'getOtherData']);
And then you inject this mocked service to $provider:
beforeEach(function () {
module(function ($provide) {
$provide.value('dataService', mockedDataService );
});
}
Instance of this mocked service can be retrieved like this then:
inject(function (dataService) {
var dataServiceInstance = dataService;
});
This will provider mocked dataService anytime it is needed. However if you need fully functional dataService you must instantiate it but always you can mock any of its dependecies.
While you can inject dependencies into the controller manually you don't need to do it as long as you have loaded the module the service belongs to.
In your case it looks like you have not loaded the ngResource module.
If you add beforeEach(module('ngResource')) to your test (and make sure the actual script file it lives in is included in Jasmine's fileset) you should not need to inject it manually.
Note that you do not need to load angular core services like $http, but since $resource is not part of core it needs to be loaded like this.
Injecting dependencies manually is mostly useful if you want to provide a mock implementation.

Mocking collaborators in Angular controller and service Jasmine tests

I have been writing some Jasmine unit tests in Angular. In the first example I'm testing a controller.
myApp.controller('MyCtrl', function($scope, Config){
...
});
I have a configuration service (Config) that keeps configuration from the database and is injected into my controller. As this is a unit test, I want to mock out that configuration service altogether, rather than allowing execution to pass through it and using $httpBackend. Examples I found taught me about a $controller function I can use like this, in order to get an instance of my controller with my mocks injected in place of the usual collaborator:
beforeEach(inject(function($controller, $rootScope){
var scope = $rootScope.$new();
var configMock = {
theOnlyPropertyMyControllerNeeds: 'value'
};
ctrl = $controller('MyCtrl', {
$scope:scope,
Config: configMock
});
}));
But I also have other services that use the Config service. To help unit test them, I assumed there would be a similar $service function I could use to instantiate a service with whatever mocks I want to provide. There isn't. I tried $injector.get, but it doesn't seem to let me pass in my mocks. After searching for a while, the best I could come up with in order to instantiate a service in isolation (avoid instantiating its collaborators) is this:
beforeEach(function() {
mockConfig = {
thePropertyMyServiceUses: 'value'
};
module(function($provide) {
$provide.value('Config', mockConfig);
});
inject(function($injector) {
myService = $injector.get('MyService');
});
});
Is this the right way? It seems to be overriding the entire application's definition of the Config service, which seems maybe like overkill.
Is it the only way? Why is there no $service helper method?
For unit testing, it is common that you override a service for the sake of testing. However, you can use $provide to override an existing service instead of using inject, as long as you load the application before hand.
Assuming that you created Config using something like:
angular.moduel('...', [...]).factory('Config', function (...) {...});
If so, try this:
...
beforeEach(module("<Name of you App>"));
beforeEach(
module(function ($provide) {
$provide.factory('Config', function (...) {...});
});
);
...
After that, when you initialise your controller, it will get the mocked Config.

Resources