I'm testing a directive ('planListing') that has a dependency on a service called 'planListingService'. This service has a dependency to another service called 'ajax' (don't shoot the messenger for the bad names).
I'm able to compile the directive, load its scope and get the controller WITH A CAVEAT. As of now I am being forced to mock both services 'planListingService' and 'ajax' otherwise I will get an error like this:
Error: [$injector:unpr] Unknown provider: ajaxProvider <- ajax <- planListingService
http://errors.angularjs.org/1.3.20/$injector/unpr?p0=ajaxProvider%20%3C-%20ajax%20%3C-%20planListingService
I thought that because I was mocking up the 'planListingService' that I wouldn't have to actually bother with any implementation nor any dependencies of this service. Am I expecting too much?
Here is the code in a nutshell:
planListing.js
angular.module('myApp')
.directive('planListing', planListing)
.controller('planListingCtrl', PlanListingCtrl);
function planListing() {
var varDirective = {
restrict: 'E',
controller: PlanListingCtrl,
controllerAs: 'vm',
templateUrl: "scripts/directives/planListing/planListing.html";
}
};
return varDirective;
}
PlanListingCtrl.$inject = ['planListingService'];
function PlanListingCtrl(planListingService) {
...
}
planListingService.js
angular.module('myApp')
.factory('planListingService', planListingService);
planListingService.$inject = ['$q', 'ajax'];
function planListingService($q, ajax) {
...
}
ajax.js
angular.module('myApp')
.factory('ajax', ['backend', '$browser', 'settings', '$http', '$log',
function (backend, $browser, settings, $http, $log) {
...
planListing.spec.js
describe('testing planListing.js',function(){
var el,ctrl,scope,vm;
var service;
module('myApp');
module('my.templates');
beforeEach(module(function ($provide){
// This seems to have no effect at all, why?
$provide.service('planListingService', function () {
this.getAllPricePlans=function(){};
});
// I don't get the error if I uncomment this:
// $provide.service('ajax', function ($q) {
// this.getAllPricePlans=function(){};
// });
}));
beforeEach(function() {
module('myApp');
module('my.templates');
});
beforeEach(angular.mock.inject(function (_$compile_,_$rootScope_,_$controller_){
$compile=_$compile_;
$rootScope = _$rootScope_;
$controller = _$controller_;
el = angular.element('<plan-listing></plan-listing>');
scope = $rootScope.$new();
$compile(el)(scope);
scope.$digest();
ctrl = el.controller('planListing');
scope = el.isolateScope() || el.scope();
vm = scope.vm;
}));
describe('testing compilation / linking', function (){
it('should have found directive and compiled template', function () {
expect(el).toBeDefined();
expect(el.html()).not.toEqual('');
expect(el.html()).toContain("plan-listing-section");
});
});
it('should have a defined controller',function(){
expect(ctrl).toBeDefined();
});
it('should have a defined scope',function(){
expect(ctrl).toBeDefined();
});
});
So why is that I need to mock up the 'ajax' service even though I am mocking up 'planListingService' which is the one calling the 'ajax' service?
Thanks!
I have been there... feels like bad start But i think your directive is depend on the service and you need to inject it in order to directive can work with this, Just by calling directive it doesn't mean that it's going to inject it in your test. It will look for it and if it's not injected it will give you error
you could do so before testing your directive
beforeEach(inject(function ($injector) {
yourService = $injector.get('yourService');
})
For documentation purposes, here is the answer (thanks #estus for noticing this):
Indeed the problem was related to the incorrect initialization of my modules. Instead of this:
describe('testing planListing.js',function(){
var el,ctrl,scope,vm;
var service;
module('myApp');
module('my.templates');
...
I should've done this:
describe('testing planListing.js',function(){
var el,ctrl,scope,vm;
var service;
beforeEach(module('myApp'));
beforeEach(module('my.templates'));
...
After that things started working again as expected.
Related
I am trying to integrate Karma and Jasmine in to my project.
I have started off with a very basic test to ensure my controller is defined and a $scope variable equals a string - which pass as expected.
My controller, also calls a service which performed a $http.get, when running my test, without any mention of a service, i get the error:
Error: Unexpected request: GET /my/endpoint/
No more request expected
Controller:
define(['module'], function (module) {
'use strict';
var MyController = function ($scope, MyService) {
$scope.testScope = 'karma is working!';
MyService.getData().then(function (data) {
$scope.result = data.hour
});
};
module.exports = ['$scope', 'MyService', MyController ];
});
Test:
define(['require', 'angular-mocks'], function (require) {
'use strict';
var angular = require('angular');
describe("<- MyController Spec ->", function () {
var controller, scope;
beforeEach(angular.mock.module('myApp'));
beforeEach(inject(function (_$controller_, _$rootScope_) {
scope = _$rootScope_.$new();
controller = _$controller_('MyController', {$scope: scope});
scope.$apply();
}));
it('should verify that the controller exists ', function() {
expect(controller).toBeDefined();
});
it('should have testScope scope equaling *karma is working*', function() {
expect(scope.testScope ).toEqual('karma is working!');
});
});
});
Are the above errors expected?
UPDATE from response below:
define(['require', 'angular-mocks'], function (require) {
'use strict';
var angular = require('angular');
describe("<- MyController Spec ->", function () {
var controller, scope, $httpBackend, myService;
beforeEach(angular.mock.module('myApp'));
beforeEach(inject(function (_$controller_, _$rootScope_, _$httpBackend_, _myService_) {
scope = _$rootScope_.$new();
$httpBackend = _$httpBackend_;
$httpBackend.expectGET("/my/endpoint");
controller = _$controller_('MyController', {$scope: scope});
scope.$apply();
}));
it('should verify that the controller exists ', function() {
expect(controller).toBeDefined();
});
it('should have testScope scope equaling *karma is working*', function() {
expect(scope.testScope ).toEqual('karma is working!');
});
});
});
Using Angular Mocks you will always get an error if there is an unexpected or incorrect http request attempted -- even for templates. In your case there are two ways to handle this for testing:
use $httpBackend
$httpBackend was designed for testing http requests without actually hitting the wire. In your test, simply add
$httpBackend.expectGET("/my/endpoint");
before you initialize the controller.
Mock the service
The service itself is making the http request, so you can mock the service instead. Services will be injected automatically as usual, but you can explicitly injection whatever you want:
controller = _$controller_('MyController', {$scope: scope,
MyService: {getData: () => ({then: () => {}}) });
This injects an object that has a getData function which returns an object with .then function. Of course this doesn't come close to implementing what you are trying to do, but it is another way to perform the test.
Both of the above approaches are valid. It depends on what you are testing and what you are trying to accomplish with the testing.
I'm getting the following error in the js console log when attempting to assign the controller to 'myCtrl' variable in a jasmine test: 'myApp.controller is not a function'
In the controller itself, the controller is defined as the following and this is what's triggering the error I mentioned:
myApp.controller('myCtrl', ...
And this is how I'm trying to access the controller in my spec file:
beforeEach(function() {
module('myApp');
});
it('tests the controller', inject(function($controller) {
var myCtrl = $controller('myCtrl');
}));
Any idea why it's still throwing this error? Seems to be a missing dependency but not sure where..
If you want to test your controller , following is one way of writing a test case
describe('YourControllerName', function () {
var $rootScope, scope, $controller ;
beforeEach(angular.mock.module('YourModuleName'));
beforeEach(inject(function ($rootScope, $controller ) {
scope = $rootScope.$new();
$controller('YourControllerName', {
$scope: scope
});
}));
it('Should do this', function () {
//assertions
});
});
Thanks - It turns out it was something as simple as I need to list myApp.js before myController.js in the SecRunner.html. I was pretty sure I tried that previously.. but there you go.
I've been trying to get started with unit testing in angular with karma and jasmine, and i've been pulling my hair out trying to wrap my head around how to test controllers with dependencies. I tried mocking a spy with a jasmine spyObj and registering it in the beforeEach hook, but for some reason the spy isn't being recognized.
Here's the code:
angular.module('testModule', [])
.controller('TestController', [
'$scope',
'TestService',
function ($scope, TestService) {
$scope.data = TestService.load();
}])
.factory('TestService', function () {
return {
load: function(){
return "foo";
}
}
});
and here's the test
describe('TestController', function() {
var $controller, $scope, TestService;
beforeEach(module('testModule'), function($provide){
TestService = jasmine.createSpyObj("TestService", ["load"]);
TestService.load.andReturn("bar");
$provide.value("TestService", TestService)
});
beforeEach(inject(function(_$controller_, $rootScope, _TestService_) {
$scope = $rootScope.$new();
TestService = _TestService_;
$controller = _$controller_('TestController', {
$scope: $scope,
TestService: TestService
});
}));
it('should set $scope.data to bar when TestService.load is called', function() {
expect(TestService.load).toHaveBeenCalled();
expect($scope.data).toEqual("bar");
}); });
Both assertions in the test fail.
I get 'Error: Expected a spy, but got Function' when i call expect(TestService.load).toHaveBeenCalled();
and if I call expect($scope.data).toEqual("bar"), I get Expected 'foo' to equal 'bar'. "Foo" is coming from the actual service, not the spy object.
Thanks for your help.
Instead of jasmine.createSpyObj, it will be easier to use the existing service that the $injector provides and then just mock the single method. You can achieve this with spyOn instead:
describe('TestController', function() {
var $controller, $scope, TestService;
beforeEach(module('testModule'));
beforeEach(inject(function(_$controller_, $rootScope, _TestService_) {
$scope = $rootScope.$new();
TestService = _TestService_;
spyOn(TestService, 'load').and.returnValue('bar');
$controller = _$controller_('TestController', {
$scope: $scope,
TestService: TestService
});
}));
it('should set $scope.data to bar when TestService.load is called', function() {
expect(TestService.load).toHaveBeenCalled();
expect($scope.data).toEqual("bar");
});
});
In your beforeEach you are injecting in _TestService_ and then overwriting the one you declared in the previous beforeEach via:
TestService = _TestService_;
Remove that code and your test should succeed.
Also there is no need to do this:
$provide.value("TestService", TestService)
Basically you're trying to use Angular's dependency injection when you're manually injecting things which is unnecessary.
Given a directive that has an external controller:
.directive('d1', function () {
return {
controller: 'd1controller',
restrict: 'E',
link: function ($scope, $element, $attributes, $controller) {
$controller.doStuff();
}
};
});
How do I mock the d1controller controller in the d1 directive's unit tests?
My attempts:
I tried with $provide as when mocking a service:
beforeEach(module('app', function ($provide) {
ctrlMock = jasmine.createSpyObj('ctrlMock', ['doStuff']);
$provide.value('d1controller', ctrlMock );
}));
And I also tried with $controllerProvider
beforeEach(module('app', function ($controllerProvider) {
ctrlMock = jasmine.createSpyObj('ctrlMock', ['doStuff']);
$controllerProvider.register('d1controller', ctrlMock);
}));
I'm the OP. It turns out that using $controllerProvider works. You have to pass it a constructor; not an instance.
beforeEach(module('app', function ($controllerProvider) {
$controllerProvider.register('d1controller', function Mock(){
this.doStuff = function(){};
});
}));
I have had decent luck creating a mock module, that contains all of my mocked services/controllers and then including that mock module after my app module. It will essentially override all of the services/controllers you have in your mock module.
Create your mock module, and your controller to mock in that module.
appMock = angular.module('appMock', []);
appMock.controller('ControllerToMockCtrl', ['$scope', function($scope) {
$scope.greeting = 'Hola!';
}]);
In your test.
beforeEach(function(){
module('app');
module('appMock');
});
This blog is a good example of this strategy. http://southdesign.de/blog/mock-angular-js-modules-for-test-di.html#using-the-mock
within a controller i have a function which uses $state.transitionTo to "redirect" to another state.
now i am stuck in testing this function, i get always the error Error: No such state 'state-two'. how can i test this? it its totally clear to me that the controller does not know anything about the other states, but how can i mock this state?
some code:
angular.module( 'mymodule.state-one', [
'ui.state'
])
.config(function config($stateProvider) {
$stateProvider.state('state-one', {
url: '/state-one',
views: {
'main': {
controller: 'MyCtrl',
templateUrl: 'mytemplate.tpl.html'
}
}
});
})
.controller('MyCtrl',
function ($scope, $state) {
$scope.testVar = false;
$scope.myFunc = function () {
$scope.testVar = true;
$state.transitionTo('state-two');
};
}
);
describe('- mymodule.state-one', function () {
var MyCtrl, scope
beforeEach(module('mymodule.state-one'));
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
MyCtrl = $controller('MyCtrl', {
$scope: scope
});
}));
describe('- myFunc function', function () {
it('- should be a function', function () {
expect(typeof scope.myFunc).toBe('function');
});
it('- should test scope.testVar to true', function () {
scope.myFunc();
expect(scope.testVar).toBe(true);
expect(scope.testVar).not.toBe(false);
});
});
});
Disclaimer: I haven't done this myself, so I totally don't know if it will work and is what your are after.
From the top of my head, two solutions come to my mind.
1.) In your tests pre configure the $stateProvider to return a mocked state for the state-two That's also what the ui-router project itself does to test state transitions.
See: https://github.com/angular-ui/ui-router/blob/04d02d087b31091868c7fd64a33e3dfc1422d485/test/stateSpec.js#L29-L42
2.) catch and parse the exception and interpret it as fulfilled test if tries to get to state-two
The second approach seems very hackish, so I would vote for the first.
However, chances are that I totally got you wrong and should probably get some rest.
Solution code:
beforeEach(module(function ($stateProvider) {
$stateProvider.state('state-two', { url: '/' });
}));
I recently asked this question as a github issue and it was answered very helpfully.
https://github.com/angular-ui/ui-router/issues/537
You should do a $rootScope.$apply() and then be able to test. Note that by default if you use templateUrl you will get an "unexpected GET request" for the view, but you can resolve this by including your templates into your test.
'use strict';
describe('Controller: CourseCtrl', function () {
// load the controller's module
beforeEach(module('myApp'));
// load controller widgets/views/partials
var views = [
'views/course.html',
'views/main.html'
];
views.forEach(function(view) {
beforeEach(module(view));
});
var CourseCtrl,
scope;
// Initialize the controller and a mock scope
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
CourseCtrl = $controller('CourseCtrl', {
$scope: scope
});
}));
it('should should transition to main.course', inject(function ($state, $rootScope) {
$state.transitionTo('main.course');
$rootScope.$apply();
expect($state.current.name).toBe('main.course');
}));
});
Also if you want to expect on that the transition was made like so
expect(state.current.name).toEqual('state-two')
then you need to scope.$apply before the expect() for it to work