Our Angular project moved to ES6 and Reduxjs, and now I am struggling to get controller unit tests working. Specifically, I cant seem to mock correctly when it comes to the class constructor. From what i have researched, i cant spyOn an ES6 class constructor, so i need to mock its dependencies and also accommodate the binding to lexical 'this' that ngRedux.connect() facilitates.
My test makes it to the connect function in the constructor, and then gives me the error: "'connect' is not a function"
I think i may have several things wrong here. If i comment out the connect line in the constructor, it'll get to my runOnLoad function and the error will tell me that fromMyActions isnt a function. this is because the redux connect function binds the actions to 'this', so given these issues, I take it I cant mock redux unless i provide its implementation. any advice? I am relatively new to angular as well - and my weakest area is unit testing and DI.
Here is my module and controller:
export const myControllerModule = angular.module('my-controller-module',[]);
export class MyController {
constructor($ngRedux, $scope) {
'ngInject';
this.ngRedux = $ngRedux;
const unsubscribe = this.ngRedux.connect(this.mapState.bind(this), myActions)(this);
$scope.$on('$destroy', unsubscribe);
this.runOnLoad();
}
mapState(state) {
return {};
}
runOnLoad() {
this.fromMyActions(this.prop);
}
}
myControllerModule
.controller(controllerId, MyController
.directive('myDirective', () => {
return {
restrict: 'E',
controllerAs: 'vm',
controller: controllerId,
templateUrl: htmltemplate
bindToController: true,
scope: {
data: '=',
person: '='
}
};
});
export default myControllerModule.name;
and my test:
import {myControllerModule,MyController} from './myController';
import 'angular-mocks/angular-mocks';
describe('test', () => {
let controller, scope;
beforeEach(function() {
let reduxFuncs = {
connect: function(){}
}
angular.mock.module('my-controller-module', function ($provide) {
$provide.constant('$ngRedux',reduxFuncs);
});
angular.mock.inject(function (_$ngRedux_, _$controller_, _$rootScope_) {
scope = _$rootScope_.$new();
redux = _$ngRedux_;
var scopeData = {
data : {"test":"stuff"},
person : {"name":"thatDude"}
} ;
scope.$digest();
controller = _$controller_(MyController, {
$scope: scope,
$ngRedux: redux
}, scopeData);
});
});
});
The idea behind Redux is that most of your controllers have no, or very little logic. The logic will be in action creators, reducers and selectors mostly.
In the example you provide, most of your code is just wiring things.
I personally don't test wiring, because it adds very little value, and those kinds of test are generally very brittle.
With that said, if you want to test your controllers nonetheless you have two options:
Use functions instead of classes for controllers. For most controllers using a class adds no real value. Instead use a function, and isolate the logic you want to test in another pure function. You can then test this function without even needing mocks etc.
If you still want to use classes, you will need to use a stub of ng-redux, (something like this: https://gist.github.com/wbuchwalter/d1448395f0dee9212b70 (it's in TypeScript))
And use it like this:
let myState = {
someProp: 'someValue'
}
let ngReduxStub;
let myController;
beforeEach(angular.mock.inject($injector => {
ngReduxStub = new NgReduxStub();
//how the state should be initially
ngReduxStub.push(myState);
myController = new MyController(ngReduxStub, $injector.get('someOtherService');
}));
Note: I personnaly don't use mapDispatchToProps (I expose my action creators through an angular service), so the stub does not handle actions, but it should be easy to add.
I have been getting the following error while trying to write tests using Karma and Jasmine:
TypeError: undefined is not a constructor (evaluating
$ngRedux.connect(function (state) { ({}, state.editAudience); }, _actions2.default)')
I managed to make it work by roughly translating wbuch's stub into es6 and using it like this:
angular.mock.module( $provide => {
ngReduxStub = new NgReduxStub()
ngReduxStub.push(myState)
const dependencies = ['$scope', '$ngRedux']
dependencies.forEach( dependency => {
if(dependency =='$ngRedux') { return $provide.value('$ngRedux', ngReduxStub)}
return $provide.value(dependency, {})
})
})
angular.mock.inject( ($compile, $rootScope, $componentController) => {
scope = $rootScope.$new()
ctrl = $componentController('csEditAudience', {$scope: scope}, {})
})
Here's my version of the stub
I hope this helps!
Related
So I am using $uibModal from bootstrap and I have the following code in my AngularJS controller:
vm.openPopup = function() {
$uibModal.open({
templateUrl: 'popup.html',
controller: function() {
var modal = this;
modal.hi = function() {
// some code here
}
}
});
};
How would I go about calling the modal.hi function in Jasmine and unit testing it to make sure it works correctly?
So the main problem with testing this code is that you've basically "buried" an anonymous function (modal.hi) inside of another anonymous function ($uibModal.open). That makes it pretty tricky to test.
You've got a few options: a.) you can mock the $uibModal service, b.) you can restructure your code, or c.) you could just drop your hi function onto the vm itself, and then call it from your tests. I think the last option would be the most expedient, but here are some examples of all three approaches.
Option 1: Mock the $uibModal service
describe('Test vm.openPopup', function () {
var mockUibModal = {
open: function(options){
var ctrl = options.controller();
// call your `hi` function:
ctrl.hi();
}
};
beforeEach(function(){
module(function($provide){
$provide.value('$uibModal', mockUibModal);
});
});
});
And from there, you could call your vm.openPopup method, and go about testing the results. Note that the module function comes from angular-mocks, which you'll need to install/include with your tests. Related question: "How do you mock a service in AngularJS when unit testing with jasmine?"
Option 2: Restructure your code
Here's a pattern that I frequently use, which involves shifting the logic/functions you wish to test into a separate factory:
var app = angular.controller('YourController', function ($uibModal, MyHelperFactory) {
var vm = this;
var modal;
var helper = MyHelperFactory(vm, modal);
vm.openPopup = function () {
$uibModal.open({
templateUrl: 'popup.html',
controller: function () {
modal = this;
modal.hi = helper.hi;
}
});
};
});
app.factory('MyHelperFactory', function () {
return function (vm, modal) {
return {
hi: function () {
// some code here, maybe it needs to reference the `vm` object, whatever...
}
}
};
})
The benefit of this approach is that you can test the MyHelperFactory on its own, without needing to instantiate YourController, and without needing to involve the $uibModal service. This is typically my favorite approach: no inline/anonymous functions - get that logic into helper factories, and out of my controllers.
Option 3: Drop the hi function onto vm
var app = angular.controller('YourController', function ($uibModal, MyHelperFactory) {
var vm = this;
// this pattern allows your function to be scoped with the not-yet-existing `modal` object
vm.hi = function (modal) {
return function () {
// some code here
}
};
vm.openPopup = function () {
$uibModal.open({
templateUrl: 'popup.html',
controller: function () {
var modal = this;
modal.hi = vm.hi(modal);
}
});
};
});
And from there, you can just test it by calling vm.hi from within your tests. I call this approach "dirty" because it adds the hi method to the vm object, and I generally avoid adding any properties to the vm object that aren't actually needed on the controller scope. In this case though, we're breaking that rule because it's the quickest/easiest way to "expose" this function that you wish to test.
I have a service MyService with a function using the ga() event tracking call which I want to test:
angular.module('myModule').factory('MyService', [function() {
var myFunc = function() {
ga('send', 'event', 'bla');
// do some stuff
}
return {
myFunc: myFunc
}
]);
My spec file looks like this:
describe('The MyService', function () {
var MyService,
ga;
beforeEach(function () {
module('myModule');
ga = function() {};
});
beforeEach(inject(function (_MyService_) {
MyService = _MyService_;
}));
it('should do some stuff', function () {
MyService.myFunc();
// testing function
});
});
Running my tests always gives me:
ReferenceError: Can't find variable: ga
The problem is global scope of ga.
The ga variable that you create inside your tests has a local scope and will not be visible to your own service.
By using a global variable (ga) you have made unit testing difficult.
The current option would be to either create a angular service to wrap gaand use that everywhere else. Such service can be mocked too.
The other option is to override the global ga. But this will have side effects.
window.ga=function() {}
After trying different solution I finally fixed with below code.
beforeAll( ()=> {
// (<any>window).gtag=function() {} // if using gtag
(<any>window).ga=function() {}
})
Slightly out of date, but I am trying to leverage ReactGA and mocked creating an event like:
it('should do something...', () => {
const gaSpy = jest.spyOn(ReactGA, 'ga');
someService.functionThatSendsEvent({ ...necessaryParams });
expect(gaSpy).toHaveBeenCalledWith('send', 'event',
expect.objectContaining({/*whatever the event object is supposed to be*/}
);
});
This is helpful if youre sending specific data to an angular/reactjs service which is then sending it to GA.
so, I'm trying to use the 1.5 component feature, but coding with fat arrows. I am using babel to build the system
My component, stripped down to the bare minimum to show my problem, is thus:
angular.module('myApp')
.component('myComponent', {
controller: () => {
this.$onInit = () => {};
},
template: `<p>foobar1</p>`
});
when I try and load this component, I get an error complaining about
typeError: Cannot set property '$onInit' of undefined
so, when I look at the sources in chrome devtools, I see
angular.module('myApp').component('myComponent', {
/** #ngInject */
controller: function controller() {
undefined.$onInit = function () {};
},
template: '<p>foobar1</p>'
});
I would expect that I have done something very wrong, but can't see it ;)
anyone got any tips ?
thanks
Angular creates new instantion of controller for every component. In ES5 we dont have classes, so we pass construction function here.
But in es6 we have class, so you can use it instead
let myComponent = {
controller: myComponentController,
template: `<p>foobar1</p>`
};
class myComponentController{
constructor() {
this.answer = 41;
}
$onInit() {
this.answer++;
}
};
angular.module('myApp')
.component('myComponent', myComponent);
Pascal has also written something about it here: http://blog.thoughtram.io/angularjs/es6/2015/01/23/exploring-angular-1.3-using-es6.html
I wish to reuse my mocks instead of having to set them up in every unit test that has them as dependency. But I'm having a hard time figuring out how to inject them properly.
Here's my attempt at unit test setup, which of course fails because ConfigServiceMockProvider doesn't exist.
describe('LoginService tests', function () {
var LoginService;
beforeEach(module('mocks'));
beforeEach(module('services.loginService', function ($provide, _ConfigServiceMock_) {
$provide.value("ConfigService", _ConfigServiceMock_);
/* instead of having to type e.g. everywhere ConfigService is used
* $provide.value("ConfigService", { 'foobar': function(){} });
*/
});
beforeEach(inject(function (_LoginService_) {
LoginService = _LoginService_;
});
}
ConfigServiceMock
angular.module('mocks').service('ConfigServiceMock', function() {
this.init = function(){};
this.getValue = function(){};
}
I realize I probably could have ConfigServiceMock.js make a global window object, and thereby not needing to load it like this. But I feel there should be a better way.
Try something like this:
describe('Using externally defined mock', function() {
var ConfigServiceMock;
beforeEach(module('mocks'));
beforeEach(module('services.configService', function($provide) {
$provide.factory('ConfigService', function() {return ConfigServiceMock;});
}));
beforeEach(module('services.loginService'));
beforeEach(inject(function (_ConfigServiceMock_) {
ConfigServiceMock = _ConfigServiceMock_;
}));
// Do not combine this call with the one above
beforeEach(inject(function (_LoginService_) {
LoginService = _LoginService_;
}));
it('should have been given the mock', function() {
expect(ConfigServiceMock).toBeDefined('The mock should have been defined');
expect(LoginService.injectedService).toBeDefined('Something should have been injected');
expect(LoginService.injectedService).toBe(ConfigServiceMock, 'The thing injected should be the mock');
});
});
According to this answer, you have to put all of your calls to module before all of your calls to inject.
This introduces a bit of a catch-22 because you have to have the reference to your ConfigServiceMock (via inject) into the spec before you can set it on the LoginService (done in the module call)
The work-around is to set an angular factory function as the ConfigService dependency. This will cause angular to lazy load the service, and by that time you will have received your reference to the ConfigServiceMock.
I'm using bindToController in a directive to have the isolated scope directly attached to the controller, like this:
app.directive('xx', function () {
return {
bindToController: true,
controller: 'xxCtrl',
scope: {
label: '#',
},
};
});
Then in the controller I have a default in case label is not specified in the HTML:
app.controller('xxCtrl', function () {
var ctrl = this;
ctrl.label = ctrl.label || 'default value';
});
How can I instantiate xxCtrl in the Jasmine unit tests so I can test the ctrl.label?
describe('buttons.RemoveButtonCtrl', function () {
var ctrl;
beforeEach(inject(function ($controller) {
// What do I do here to set ctrl.label BEFORE the controller runs?
ctrl = $controller('xxCtrl');
}));
it('should have a label', function () {
expect(ctrl.label).toBe('foo');
});
});
Check this to test the issue
In Angular 1.3 (see below for 1.4+)
Digging into the AngularJS source code I found an undocumented third argument to the $controller service called later (see $controller source).
If true, $controller() returns a Function with a property instance on which you can set properties.
When you're ready to instantiate the controller, call the function and it'll instantiate the controller with the properties available in the constructor.
Your example would work like this:
describe('buttons.RemoveButtonCtrl', function () {
var ctrlFn, ctrl, $scope;
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrlFn = $controller('xxCtrl', {
$scope: scope,
}, true);
}));
it('should have a label', function () {
ctrlFn.instance.label = 'foo'; // set the value
// create controller instance
ctrl = ctrlFn();
// test
expect(ctrl.label).toBe('foo');
});
});
Here's an updated Plunker (had to upgrade Angular to make it work, it's 1.3.0-rc.4 now): http://plnkr.co/edit/tnLIyzZHKqPO6Tekd804?p=preview
Note that it's probably not recommended to use it, to quote from the Angular source code:
Instantiate controller later: This machinery is used to create an
instance of the object before calling the controller's constructor
itself.
This allows properties to be added to the controller before the
constructor is invoked. Primarily, this is used for isolate scope
bindings in $compile.
This feature is not intended for use by applications, and is thus not
documented publicly.
However the lack of a mechanism to test controllers with bindToController: true made me use it nevertheless.. maybe the Angular guys should consider making that flag public.
Under the hood it uses a temporary constructor, we could also write it ourselves I guess.
The advantage to your solution is that the constructor isn't invoked twice, which could cause problems if the properties don't have default values as in your example.
Angular 1.4+ (Update 2015-12-06):
The Angular team has added direct support for this in version 1.4.0. (See #9425)
You can just pass an object to the $controller function:
describe('buttons.RemoveButtonCtrl', function () {
var ctrl, $scope;
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('xxCtrl', {
$scope: scope,
}, {
label: 'foo'
});
}));
it('should have a label', function () {
expect(ctrl.label).toBe('foo');
});
});
See also this blog post.
Unit Testing BindToController using ES6
If using ES6,you can import the controller directly and test without using angular mocks.
Directive:
import xxCtrl from './xxCtrl';
class xxDirective {
constructor() {
this.bindToController = true;
this.controller = xxCtrl;
this.scope = {
label: '#'
}
}
}
app.directive('xx', new xxDirective());
Controller:
class xxCtrl {
constructor() {
this.label = this.label || 'default value';
}
}
export default xxCtrl;
Controller Test:
import xxCtrl from '../xxCtrl';
describe('buttons.RemoveButtonCtrl', function () {
let ctrl;
beforeEach(() => {
xxCtrl.prototype.label = 'foo';
ctrl = new xxCtrl(stubScope);
});
it('should have a label', () => {
expect(ctrl.label).toBe('foo');
});
});
see this for more information:
Proper unit testing of
Angular JS
applications with ES6 modules
In my view, this controller is not meant to be tested in isolation, because it will never work in isolation:
app.controller('xxCtrl', function () {
var ctrl = this;
// where on earth ctrl.lable comes from???
ctrl.newLabel = ctrl.label || 'default value';
});
It is tightly coupled with the directive relying on receiving its scope properties. It is not re-usable. From looking at this controller, I have to wonder where this variable is coming from. It is no better than a leaky function internally using a variable from outside scope:
function Leaky () {
... many lines of code here ...
// if we are here we are too tired to notice the leakyVariable:
importantData = process(leakyVariable);
... mode code here ...
return unpredictableResult;
}
Now I have a leaky function whose behaviour is highly unpredictable based on the variable leakyVariable present (or not) in whatever scope the function is called.
Unsurprisingly this function is nightmare to test. Which is actually a good thing, perhaps to force the developer to rewrite the function into something more modular and re-usable. Which is not hard really:
function Modular (outsideVariable) {
... many lines of code here ...
// no need to hit our heads against the wall to wonder where the variable comes from:
importantData = process(outsideVariable);
... mode code here ...
return predictableResult;
}
No leaky issues and really easy to test and re-use. Which to me tells that using the good old $scope is a better way:
app.controller('xxCtrl', function ($scope) {
$scope.newLabel = $scope.label || 'default value';
});
Simple, short and easy to test. Plus no bulky directive object definition.
The original reasoning behind the controllerAs syntax was the leaky scope inherited from the parent. However, directive's isolated scope already solves this problem. Thus I don't see any reason to use the bulkier leaky syntax.
I've found a way that is not particulary elegant but works at least (if there's a better option leave a comment).
We set the value that "comes" from the directive, and then we call the controller function again to test whatever it does. I've made a helper "invokeController" to be more DRY.
For example:
describe('buttons.RemoveButtonCtrl', function () {
var ctrl, $scope;
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('xxCtrl', {
$scope: scope,
});
}));
it('should have a label', function () {
ctrl.label = 'foo'; // set the value
// call the controller again with all the injected dependencies
invokeController(ctrl, {
$scope: scope,
});
// test whatever you want
expect(ctrl.label).toBe('foo');
});
});
beforeEach(inject(function ($injector) {
window.invokeController = function (ctrl, locals) {
locals = locals || {};
$injector.invoke(ctrl.constructor, ctrl, locals);
};
}));