I am trying to test an angularjs service called MyService. If I try to inject it, seems that angular tries to use it before is loaded. On the other hand, if I mock MyService via $provide and so on, it works but I will not have the actual object to test.
(function (angular) {
'use strict';
angular.module('app', []).run(["MyService",
function (MyService) {
MyService.initListeners();
}
]);
// this is supposed to be in another file
angular.module('app')
.service("MyService", function() {
return {
initListeners: function() {
console.log("working")
}
}
})
}(angular));
The test is this:
(function () {
'use strict';
describe("MyService", function () {
var MyService = null;
beforeEach(module("app"));
beforeEach(inject(function ($injector) {
MyService = $injector.get('MyService');
}));
afterEach(function () {
MyService = null;
});
it("injection works", function () {
expect(true).toBeTruthy(); // throws exception
});
});
}());
I did the test on a jsfiddle.
I see the order of execution with some console messages. The order of execution is the correct, as expected.
My service constructor
before init
MyService.initListeners()
after init
And the two test work correctly:
it("injection works", function () {
expect(true).toBeTruthy(); // throws exception
expect(MyService).toBeDefined();
});
Here is the code: http://jsfiddle.net/jordiburgos/1efvof3k/
It could be your AngularJS version, Jasmine, etc...
Related
I've searched the internet but didn't find a solution that works for me. The situation is that I have a component with a controller. I can use Jasmine to test that the controller is created and I can also call the $onInit hook without a problem.
However, when I try to call a component function that uses a service function that returns a promise, things fail miserably. I'm using the following versions:
AngularJS v1.5.11
Jasmine v3.3.0
Karma v3.1.4
My simplified component is:
(function ()
{
"use strict";
angular
.module("app")
.component("MyComponent", {
controller: "MyController"
})
.controller("MyController", MyController);
function MyController(MyService)
{
"ngInject";
var vm = this;
vm.$onInit = $onInit;
vm.onPaginate = onPaginate;
function $onInit()
{
vm.data = [];
}
function onPaginate()
{
getData();
}
function getData()
{
MyService.getData().then(onComplete);
function onComplete(response)
{
vm.data = response.list;
}
}
}
})();
The function MyService.getData queries a REST API to retrieve some data. It will return a promise that gets resolved when the data is received. This all works in the application.
I want to test the MyController.onPaginate function and wrote the following Jasmine test spec:
describe("MyComponent: MyController", function ()
{
var $componentController, $rootScope, $q, $scope;
var componentBindings = {
};
var data = {
list: [{
"id": 23,
}],
total_count: 1
};
var mock_MyService = {
getData: function ()
{
return $q.resolve(data);
}
};
beforeEach(function ()
{
angular.mock.module("app", function ($provide)
{
$provide.value("MyService", mock_MyService);
});
angular.mock.inject(function ($injector)
{
$q = $injector.get("$q");
$componentController = $injector.get("$componentController");
$rootScope = $injector.get("$rootScope");
$scope = $rootScope.$new();
});
});
it("should retrieve data", function ()
{
var $ctrl = $componentController("MyComponent", { $scope: $scope, MyService: mock_MyService }, componentBindings);
$ctrl.$onInit();
$ctrl.onPaginate($ctrl.tableState);
$scope.$apply();
expect($ctrl.data.length).toEqual(1);
});
});
When I run this I get an error:
TypeError: e is not a function
at <Jasmine>
at b.$apply (node_modules/angular/angular.min.js:147:388)
at UserContext.<anonymous> (myComponent.spec.js:...)
at <Jasmine>
So how can I test this properly?
After a week looking for a good answer/sample, I decided to post my question.
I need to know how is the best way to code and test something like this:
Controller
// my.controller.js
(function () {
'use strict';
angular.module('myApp.myModule').controller('Awesome', Awesome);
function Awesome($http, $state, AwesomeService) {
var vm = this; // using 'controllerAs' style
vm.init = init;
vm.awesomeThingToDo = awesomeThingToDo;
vm.init();
function awesomeThingToDo() {
AwesomeService.awesomeThingToDo().then(function (data) {
vm.awesomeMessage = data.awesomeMessage;
});
}
function init() {
vm.awesomeThingToDo(); // Should be ready on page start
}
}
})();
Service
// my.service.js
(function () {
'use strict';
angular.module('myApp.myModule').factory('AwesomeService', AwesomeService);
function AwesomeService($resource, $http) {
var service = {
awesomeThingToDo: awesomeThingToDo
}
return service;
function awesomeThingToDo() {
var promise = $http.get("/my-backend/api/awesome").then(function (response) {
return response.data;
});
return promise;
}
}
})();
My app works OK with this structure. And my Service unit tests are OK too.
But I don't know how to do unit tests on Controller.
I tried something like this:
Specs
// my.controller.spec.js
(function () {
'use strict';
describe("Awesome Controller Tests", function() {
beforeEach(module('myApp.myModule'));
var vm, awesomeServiceMock;
beforeEach(function () {
awesomeServiceMock = { Is this a good (or the best) way to mock the service?
awesomeThingToDo: function() {
return {
then: function() {}
}
}
};
});
beforeEach(inject(function ($controller) {
vm = $controller('Awesome', {AwesomeService : awesomeServiceMock});
}));
it("Should return an awesome message", function () {
// I don't know another way do to it... :(
spyOn(awesomeServiceMock, "awesomeThingToDo").and.callFake(function() {
return {
then: function() {
vm.awesomeMessage = 'It is awesome!'; // <-- I think I shouldn't do this.
}
}
});
vm.awesomeThingToDo(); // Call to real controller method which should call the mock service method.
expect(vm.awesomeMessage).toEqual('It is awesome!'); // It works. But ONLY because I wrote the vm.awesomeMessage above.
});
});
})();
My app uses Angular 1.2.28 and Jasmine 2.1.3 (with Grunt and Karma).
UPDATE: Solved!
it("Should return an awesome message", function () {
// Solved with callback parameter
spyOn(awesomeServiceMock, "awesomeThingToDo").and.callFake(function(callback) {
return {
then: function(callback) {
callback({awesomeMessage: 'It is awesome!'}); //callback call works fine! :D
}
}
});
I updated the question with a possible (bad) solution:
it("Should return an awesome message", function () {
// Solved with callback parameter
spyOn(awesomeServiceMock, "awesomeThingToDo").and.callFake(function(callback) {
return {
then: function(callback) {
callback({awesomeMessage: 'It is awesome!'}); //callback call works fine! :D
}
}
});
I used a callback to pass the mocked parameter and call the real implementation. :D
No, that's not how I would do this.
First, there is no need to create a mock service: you can inject the real one, and spy on it.
Second, Angular has everything you need to create promises and to resolve them. No need to create fake objects with a fake then() function.
Here's how I would do it:
describe("Awesome Controller Tests", function() {
beforeEach(module('myApp.myModule'));
var vm, awesomeService, $q, $rootScope;
beforeEach(inject(function($controller, _awesomeService_, _$q_, _$rootScope_) {
$q = _$q_;
awesomeService = _awesomeService_;
$rootScope = _$rootScope_;
vm = $controller('Awesome');
}));
it("Should return an awesome message", function () {
spyOn(awesomeService, "awesomeThingToDo").and.returnValue(
$q.when({
awesomeMessage: 'awesome message'
}));
vm.awesomeThingToDo();
// at this time, the then() callback hasn't been called yet:
// it's called at the next digest loop, that we will trigger
$rootScope.$apply();
// now the then() callback should have been called and initialized
// the message in the controller with the message of the promise
// returned by the service
expect(vm.awesomeMessage).toBe('awesome message');
});
});
Unrelated note: 1.2.28 is quite old. You should migrate to the latest version.
I'm having trouble getting my tests to run due to dependencies not beeing injected correctly.
The error I'm getting is defined in the title. I've included the actual test code, the app.js & index.html file from my solution.
The problem lies with the deferred bootstrap which I'm not fimiliar with as it was included by one of my colleagues. If I remove the "app.config(function (STARTUP_CONFIG..." from the app.js file then the test runs fine
How can I correctly inject the STARTUP_CONFIG in my test?
test code
..
..
describe("test description...", function () {
var app;
var mockupDataFactory;
beforeEach(module('Konstrukt'));
beforeEach(inject(function (STARTUP_CONFIG,BUDGETS,APPLICATIONS) {
//failed attempt to inject STARTUP_CONFIG
}));
beforeEach(function () {
app = angular.module("Konstrukt");
});
beforeEach(function () {
mockupDataFactory = konstruktMockupData.getInstance();
});
it('should be accessible in app module', function () {
expect(app.pivotTableService).toNotBe(null); //this test runs fine
});
it('test decr...', inject(function ( pivotTableService) {
... //Fails here
..
..
app.js
..
..
angular.module('Konstrukt', ['ngGrid', 'ngSanitize', 'ngRoute','pasvaz.bindonce', 'ngAnimate', 'nvd3ChartDirectives', 'ui.select', 'ngProgress', 'ui.grid', 'ui.grid.edit','ui.grid.selection', 'ui.grid.cellNav', 'ui.grid.pinning', 'ui.grid.resizeColumns']);
var app = angular.module('Konstrukt');
app.config(function (STARTUP_CONFIG, BUDGETS, APPLICATIONS) {
var STARTUP_CONFIG = STARTUP_CONFIG;
var BUDGETS = BUDGETS;
var APPLICATIONS = APPLICATIONS;
});
..
..
index.html
..
..
<script>
setTimeout(function(){
window.deferredBootstrapper.bootstrap({
element: window.document.body,
module: 'Konstrukt',
resolve: {
STARTUP_CONFIG: ['$http', function ($http) {
return $http.get('/scripts/_JSON/activeBudgets.JSON');
}],
BUDGETS: ['$http', function ($http) {
return $http.get('/scripts/_JSON/activeBudgets.JSON');
}],
APPLICATIONS: ['$http', function ($http) {
return $http.get('/scripts/_JSON/applications.JSON');
}]
}
})
} , 1500);
</script>
The deferredBootstrapper will not run in your unit tests, which means the constants it normally adds to your module won't be available.
You can add a global beforeEach that provides mocked versions of them:
beforeEach(function () {
module(function ($provide) {
$provide.constant('STARTUP_CONFIG', { something: 'something' });
$provide.constant('BUDGETS', { something: 'something' });
$provide.constant('APPLICATIONS', { something: 'something' });
});
});
I have a service which is being tested:
angular.module('services', []).
service('myService', function(topService) {});
And the topService is another angularjs service.
Now I want to write unit test for myService, but I need to mock the topService and pass it to myService.
I tried but not sure how to make it work:
define(['angular', 'angularMocks', 'services/my'], function(_, mocks, _) {
beforeEach(mocks.module('services'));
describe("my service", function() {
it("should do something", mocks.inject(function(myService) {
// how to mock and inject a `topService` to `myService` ????
expect(myService).doSomething().toEqual("???");
}));
});
});
How to do that?
First create the mockedTopService, provide any function which will be needed by the test to run:
var mockedtopService = {
doSomething: function() {
return "test";
}
};
Then provide it to angular using $provide:
beforeEach(function () {
module(function ($provide) {
$provide.value('topService', mockedtopService );
});
}
In case you need to get an instance of the service via angular you can do it this way:
inject(function (topService) {
var topServiceInstance = topService;
});
I have a ParseService, that I would like to mock in order test all the controllers that are using it, I have been reading about jasmine spies but it is still unclear for me. Could anybody give me an example of how to mock a custom service and use it in the Controller test?
Right now I have a Controller that uses a Service to insert a book:
BookCrossingApp.controller('AddBookCtrl', function ($scope, DataService, $location) {
$scope.registerNewBook = function (book) {
DataService.registerBook(book, function (isResult, result) {
$scope.$apply(function () {
$scope.registerResult = isResult ? "Success" : result;
});
if (isResult) {
//$scope.registerResult = "Success";
$location.path('/main');
}
else {
$scope.registerResult = "Fail!";
//$location.path('/');
}
});
};
});
The service is like this:
angular.module('DataServices', [])
/**
* Parse Service
* Use Parse.com as a back-end for the application.
*/
.factory('ParseService', function () {
var ParseService = {
name: "Parse",
registerBook: function registerBook(bookk, callback) {
var book = new Book();
book.set("title", bookk.title);
book.set("description", bookk.Description);
book.set("registrationId", bookk.RegistrationId);
var newAcl = new Parse.ACL(Parse.User.current());
newAcl.setPublicReadAccess(true);
book.setACL(newAcl);
book.save(null, {
success: function (book) {
// The object was saved successfully.
callback(true, null);
},
error: function (book, error) {
// The save failed.
// error is a Parse.Error with an error code and description.
callback(false, error);
}
});
}
};
return ParseService;
});
And my test so far look like this:
describe('Controller: AddBookCtrl', function() {
// // load the controller's module
beforeEach(module('BookCrossingApp'));
var AddBookCtrl, scope, book;
// Initialize the controller and a mock scope
beforeEach(inject(function($controller, $rootScope) {
scope = $rootScope;
book = {title: "fooTitle13"};
AddBookCtrl = $controller('AddBookCtrl', {
$scope: scope
});
}));
it('should call Parse Service method', function () {
//We need to get the injector from angular
var $injector = angular.injector([ 'DataServices' ]);
//We get the service from the injector that we have called
var mockService = $injector.get( 'ParseService' );
mockService.registerBook = jasmine.createSpy("registerBook");
scope.registerNewBook(book);
//With this call we SPY the method registerBook of our mockservice
//we have to make sure that the register book have been called after the call of our Controller
expect(mockService.registerBook).toHaveBeenCalled();
});
it('Dummy test', function () {
expect(true).toBe(true);
});
});
Right now the test is failing:
Expected spy registerBook to have been called.
Error: Expected spy registerBook to have been called.
What I am doing wrong?
What I was doing wrong is not injecting the Mocked Service into the controller in the beforeEach:
describe('Controller: AddBookCtrl', function() {
var scope;
var ParseServiceMock;
var AddBookCtrl;
// load the controller's module
beforeEach(module('BookCrossingApp'));
// define the mock Parse service
beforeEach(function() {
ParseServiceMock = {
registerBook: function(book) {},
getBookRegistrationId: function() {}
};
});
// inject the required services and instantiate the controller
beforeEach(inject(function($rootScope, $controller) {
scope = $rootScope.$new();
AddBookCtrl = $controller('AddBookCtrl', {
$scope: scope,
DataService: ParseServiceMock
});
}));
it('should call registerBook Parse Service method', function () {
var book = {title: "fooTitle"}
spyOn(ParseServiceMock, 'registerBook').andCallThrough();
//spyOn(ParseServiceMock, 'getBookRegistrationId').andCallThrough();
scope.registerNewBook(book);
expect(ParseServiceMock.registerBook).toHaveBeenCalled();
//expect(ParseServiceMock.getBookRegistrationId).toHaveBeenCalled();
});
});
You can inject your service and then use spyOn.and.returnValue() like this:
beforeEach(angular.mock.module('yourModule'));
beforeEach(angular.mock.inject(function($rootScope, $controller, ParseService) {
mock = {
$scope: $rootScope.$new(),
ParseService: ParseService
};
$controller('AddBookCtrl', mock);
}));
it('should call Parse Service method', function () {
spyOn(mock.ParseService, "registerBook").and.returnValue({id: 3});
mock.$scope.registerNewBook();
expect(mock.ParseService.registerBook).toHaveBeenCalled();
});
Following Javito's answer 4 years after-the-fact. Jasmine changed their syntax in 2.0 for calling through to real methods on spies.
Change:
spyOn(ParseServiceMock, 'registerBook').andCallThrough();
to:
spyOn(ParseServiceMock, 'registerBook').and.callThrough();
Source
Include angular-mocks.js in your project and read carefully through the following link.