In my controller we have define the following two methods -
function goToHome() {
$state.go('app.home', {newReleaseIds: vm.newReleaseIds});
}
function createAnotherFuelRelease() {
// GA -- start creating another fuel release
$analytics.eventTrack('Start creating another fuel release', {category: 'Iron', label: moment().format('MMMM Do YYYY, h:mm:ss a')});
$state.go('app.create-iron', {selectedLocation: vm.selectedLocation, fuelReleaseNumber: vm.fuelReleaseNumber + 1, newReleaseIds: vm.newReleaseIds, pricing: $state.params.pricing});
}
called this two method from controller -
vm.createAnotherFuelRelease = createAnotherFuelRelease;
vm.goToHome = goToHome;
Now I would like to test those method from spec.js files -
it('should check goToHome()', function() {
// spyOn($state, 'go');
// $scope.inviteMembers(1);
// expect($state.go).toHaveBeenCalledWith('invite', {deptId: 1});
var spy = sinon.spy();
scope.vm.goToHome = {goToHome : spy};
scope.$digest();
expect($state.go).toHaveBeenCalledWith('app.home', {newReleaseIds: 1});
// expect(spy.calledOnce).toEqual(false);
// $compiledElement.find('.nv-button.test-gotToHome').trigger('click');
// expect(spy.calledOnce).toEqual(true);
// spy.reset();
});
but it doesn't work. If anyone knows that stuff please let me know.
Here you can find an example. It works with $location service but the idea is the same.
Replace $location with $state and be sure to load its module.
'use strict';
angular.module('test', []);
angular.module('test').controller('ctrl', function ctrlFactory($location) {
var vm = this;
vm.greet = function() {
$location.path('foo');
};
});
beforeEach(module('test'));
it('', inject(function($injector) {
// Given
var $controller = $injector.get('$controller');
var $location = $injector.get('$location');
var scope = $injector.get('$rootScope').$new();
spyOn($location, 'path');
var ctrl = $controller('ctrl', { $scope: scope });
// When
scope.$apply();
ctrl.greet();
//Then
expect($location.path).toHaveBeenCalled();
}));
Related
I am unit testing a function in a controller which is using a translate filter
I want to mock the filter with a custom value
I am using $provide for mocking a value but it does nothing
Any advices ?
Thank you very much
My controller :
function MainCtrl($scope,$filter){
$scope.onDateChange = onDateChange;
var date = new Date();
var date_format_filter = $filter("i18nFilter")("DATE_FORMAT_FILTER");
// i want this to be dd/mm/yyyy
var date_format_moment = $filter("i18nFilter")("DATE_FORMAT_MOMENT");
// i want this to be DD/MM/YYYY
console.log("DATE FORMAT" , date_format_moment); // print DATE_FORMAT_MOMENT in unit test = test fail
$scope.startDate = $filter('date')(date , date_format_filter);
$scope.endDate = $filter('date')(date , date_format_filter);
function onDateChange(whichDate){
var startDateFormatted = moment($scope.startDate , date_format_moment).startOf('day').toDate();
var endDateFormatted = moment($scope.endDate, date_format_moment).startOf('day').toDate();
if(startDateFormatted.getTime() > endDateFormatted.getTime()){
$scope.startDateError = true;
$scope.errorLabel = $filter("i18nFilter")("DATE_START_ERROR");
}else {
$scope.monitoring.startDateError = false;
}
}
}
My unit test
describe('CONTROLLER : MainCtrl', function() {
// MAIN VARIABLES ==================================================================
var $scope,
$controller,
$rootScope,
$filter;
// LOAD APP MODULE =================================================================
beforeEach(module('myApp'));
// SETUP ===========================================================================
beforeEach(function() {
module(function($provide) {
$provide.value('i18nFilter', 'DD/MM/YYYY');
});
inject(function ($rootScope , _$controller_ , $injector) {
$scope = $rootScope.$new();
$filter = $injector.get("$filter");
$controller = _$controller_;
$controller = $controller('MainCtrl' , {$scope : $scope , $filter : $filter});
});
});
// Controller initialization -------------------------------------------------------
it('- Controller should be defined.', function() {
expect($controller).toBeDefined();
});
it('- Should test startDate change event KO .', inject(function($controller){
var date_format_moment = 'DD/MM/YYYY';
$scope.startDate = moment(new Date()).add(1, 'days').format(date_format_moment);
$scope.endDate = moment(new Date()).format(date_format_moment);
$scope.onDateChange('startDate');
expect($scope.startDateError).toEqual(true);
}));
});
A filter is a function, and filter services have Filter suffix, so it should be i18nFilterFilter.
beforeEach(module({ i18nFilterFilter: jasmine.createSpy() }));
...
i18nFilterFilter.and.returnValues('dd/mm/yyyy', 'DD/MM/YYYY');
A cleaner approach is to reduce the number of moving parts and mock $filter itself,
beforeEach(module({ $filter: jasmine.createSpy() }));
...
var i18nFilterMock = jasmine.createSpy().and.returnValues('dd/mm/yyyy', 'DD/MM/YYYY');
var dateFilterMock = ...;
$filter.and.returnValues(i18nFilterMock, i18nFilterMock, dateFilterMock, dateFilterMock);
...
var ctrl = $controller('MainCtrl', ...);
expect($filter.calls.count()).toBe(4);
expect($filter.calls.allArgs()).toEqual([
['i18nFilter'], ['i18nFilter'], ['date'], ['date']
]);
expect(i18nFilterMock.calls.count()).toBe(2);
expect($filter.calls.allArgs()).toEqual([
['DATE_FORMAT_FILTER'], ['DATE_FORMAT_MOMENT']
]);
It should be mentioned that Sinon provides many more features for 'smart' stubs/spies than Jasmine, so both can be used together.
I have a service that adds/removes classes to a couple of HTML elements.
I am trying to test these changes depending on which method is called.
define(['require', 'angular'], function (require, angular) {
'use strict';
var myFactory = function () {
var header = angular.element("#app-header");
var footer = angular.element(document.getElementsByClassName("app-footer"));
var change = false;
return {
red: function() {
header.addClass("alert-warning");
footer.removeClass("notify");
change = true;
},
black: function() {
if (change) {
this.red();
}
}
};
};
return myFactory;
});
I ahve tried:
describe('<-- MyFactory Spec ------>', function () {
var myFactory, $compile, scope;
beforeEach(angular.mock.module('MyApp'));
beforeEach(inject(function(_myFactory_, _$compile_, _$rootScope_){
myFactory = _myFactory_;
$compile = _$compile_;
scope = _$rootScope_.$new();
}));
it('should open the menu', function(){
var header = angular.element("#app-header");
header = $compile(header)(scope);
scope.$digest();
myFactory.red();
scope.$apply();
expect(header).toHaveClass('alert-warning');
expect(change).toBeTruthy();
});
});
With the above, i get error:
TypeError: 'undefined' is not a function (evaluating 'expect(header).toHaveClass('alert-warning')')
I suspect you aren't pulling in jasmine-jquery matchers.
.toHaveClass(...)
Is not a standard Jasmine matcher, you need to add it with jasmine-jquery
So Im trying to figure out how to write unit tests for my angular controller. I am using karma as my runner. I was able to write 1 successful test but every time I try to write another test it yells at me about unexpected calls and such.
Here is my controller im trying to test.
(function (angular) {
'use strict';
var ngModule = angular.module('myApp.dashboardCtrl', []);
ngModule.controller('dashboardCtrl', function ($scope, $http) {
//"Global Variables"
var vm = this;
vm.success = false;
vm.repos = [];
//"Global Functions"
vm.addRepository = addRepository;
vm.listRepos = listRepos;
//Anything that needs to be instantiated on page load goes in the init
function init() {
listRepos();
}
init();
// Add a repository
function addRepository(repoUrl) {
$http.post("/api/repo/" + encodeURIComponent(repoUrl)).then(function (){
vm.success = true;
vm.addedRepo = vm.repoUrl;
vm.repoUrl = '';
listRepos();
});
}
//Lists all repos
function listRepos() {
$http.get('/api/repo').then( function (response){
vm.repos = response.data;
});
}
});
}(window.angular));
So I have a test written for listRepos(). It goes as follows
describe('dashboardCtrl', function() {
var scope, httpBackend, createController;
// Set up the module
beforeEach(module('myApp'));
beforeEach(inject(function($rootScope, $httpBackend, $controller) {
httpBackend = $httpBackend;
scope = $rootScope.$new();
createController = function() {
return $controller('dashboardCtrl', {
'$scope': scope
});
};
}));
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
httpBackend.verifyNoOutstandingRequest();
});
it('should call listRepos and return all repos from the database', function() {
var controller = createController();
var expectedResponse = [{id: 12345, url: "https://github.com/myuser/myrepo.git"}];
httpBackend.expect('GET', '/api/repo')
.respond(expectedResponse);
httpBackend.flush();
scope.$apply(function() {
scope.listRepos;
});
expect(controller.repos).toEqual(expectedResponse);
});
This works and the test passes. Now my problem is I want to write another test to test the other function that calls a new api endpoint.
This is the test im trying to write for addRepository.
it('should addRepository to the database', function() {
var controller = createController();
var givenURL = "https://github.com/myuser/myURLtoMyRepo.git";
httpBackend.expect('POST', '/api/repo/' + encodeURIComponent(givenURL)).respond('success');
httpBackend.flush();
scope.$apply(function() {
scope.addRepository(givenURL);
});
expect(controller.success).toBe(true);
expect(controller.listRepos).toHaveBeenCalled();
});
The error I get when I add this test to the spec is:
Error: Unexpected request: GET /api/repo
Expected POST /api/repo/https%3A%2F%2Fgithub.com%2Fmyuser%2FmyURLtoMyRepo.git
at $httpBackend
Error: [$rootScope:inprog] $digest already in progress
http://errors.angularjs.org/1.4.8/$rootScope/inprog?p0=%24digest
The example I am working with is this one here
Any suggestions or tips is greatly appreciated!
UPDATE:
So changed my function to return the promise from the $http.post,
I rewrote my 2nd test and also wrapped my first test in a describe block describing the function its trying to test.
With the following:
describe('addRepository', function () {
it('should addRepository to the database', function () {
var controller = createController();
var givenURL = "https://github.com/myuser/myURLtoMyRepo.git";
httpBackend.expect('POST', '/api/repo/' + encodeURIComponent(givenURL)).respond('success');
scope.$apply(function () {
scope.addRepository(givenURL);
});
httpBackend.flush();
expect(controller.success).toBe(true);
});
it('should call listRepos', function() {
var controller = createController();
httpBackend.expect('GET', '/api/repo').respond('success');
controller.controller().then(function (result) {
expect(controller.listRepos).toHaveBeenCalled();
});
httpBackend.flush();
});
});
I still get the error:
Error: Unexpected request: GET /api/repo
Expected POST /api/repo/https%3A%2F%2Fgithub.com%2Fmyuser%2FmyURLtoMyRepo.git
at $httpBackend
Error: [$rootScope:inprog] $digest already in progress
but also
TypeError: 'undefined' is not a function (evaluating 'controller.controller()')
Error: Unflushed requests: 1
which shows 2 tests failed.
The flush should come after the call to the function. I'd also change the function to return the promise from the $http.post:
// Add a repository
function addRepository(repoUrl) {
return $http.post("/api/repo/" + encodeURIComponent(repoUrl)).then(function (){
vm.success = true;
vm.addedRepo = vm.repoUrl;
vm.repoUrl = '';
listRepos();
});
}
And then in the test you can call it and test the success part:
EDIT
I changed the controller.controller() to what you have.
it('should call listRepos', function() {
// Your setup
ctrl.addRepository().then(function(result) {
expect(ctrl.listRepos).toHaveBeenCalled();
});
});
EDIT 2
I emulated as best i could your code and the tests I write for the code:
(function () {
'use strict';
angular
.module('myApp')
.controller('DashboardController',DashboardController);
DashboardController.$inject = ['$http'];
function DashboardController($http) {
var vm = this;
vm.success = false;
vm.repos = [];
vm.addRepository = addRepository;
vm.listRepos = listRepos;
init();
// Anything that needs to be instantiated on page load goes in the init
function init() {
vm.listRepos();
}
// Add a repository
function addRepository(repoUrl) {
return $http.post('http://jsonplaceholder.typicode.com/posts/1.json').then(function (){
vm.success = true;
vm.addedRepo = vm.repoUrl;
vm.repoUrl = '';
vm.listRepos();
});
}
// Lists all repos
function listRepos() {
return $http.get('http://jsonplaceholder.typicode.com/posts/1').then( function (response){
vm.repos = response.data;
});
}
};
}());
Here I'm using an online JSONPlaceholder API to simulate HTTP calls as I, obviously, can't hit what you're pointing at. And for the test (which all pass):
(function() {
'use strict';
fdescribe('DashBoardController', function() {
var $rootScope, scope, ctrl, $httpBackend;
beforeEach(module('myApp'));
beforeEach(inject(function(_$rootScope_, _$httpBackend_,$controller) {
$rootScope = _$rootScope_;
scope = $rootScope.$new();
$httpBackend =_$httpBackend_;
ctrl = $controller('DashBoardController',{
$scope: scope
});
}));
beforeEach(function() {
// Setup spies
spyOn(ctrl,'listRepos');
});
describe('controller', function() {
it('should be defined', function() {
expect(ctrl).toBeDefined();
});
it('should initialize variables', function() {
expect(ctrl.success).toBe(false);
expect(ctrl.repos.length).toBe(0);
});
});
describe('init', function() {
it('should call listRepos', function() {
$httpBackend.expectGET('http://jsonplaceholder.typicode.com/posts/1')
.respond({success: '202'});
$httpBackend.expectPOST('http://jsonplaceholder.typicode.com/posts/1.json')
.respond({success: '202'});
ctrl.addRepository().then(function(result) {
expect(ctrl.success).toBe(true);
expect(ctrl.repoUrl).toBe('');
expect(ctrl.listRepos).toHaveBeenCalled();
});
$httpBackend.flush();
});
});
});
}());
Since I wrote firebase-factory separately from RecipeController, I have an error in my Test.
TypeError: Cannot read property '$loaded' of undefined.
$loaded is a method in firebase...
test.js
describe('RecipeController', function() {
beforeEach(module('leChef'));
var $controller;
beforeEach(inject(function(_$controller_){
$controller = _$controller_;
}));
describe("$scope.calculateAverage", function() {
it("calculates average correctly", function() {
var $scope = {};
var controller = $controller('RecipeController', { $scope: $scope });
$scope.calculateAverage();
expect(average).toBe(sum/(Recipes.reviews.length-1));
});
});
});
firebase-factory.js
app.factory("Recipes", ["$firebaseArray",
function($firebaseArray) {
var ref = new Firebase("https://fiery-inferno-8595.firebaseio.com/recipes/");
return $firebaseArray(ref);
}
]);
recipe-controller.js
app.controller("RecipeController", ["$scope", "toastr", "$location", "$routeParams", "$compile", "Recipes",
function($scope, toastr, $location, $routeParams, $compile, Recipes) {
$scope.recipes.$loaded().then(function(payload) {
$scope.recipe = payload.$getRecord($routeParams.id);
$scope.html = $scope.recipe.instructions;
if (typeof $scope.recipe.reviews === "undefined") {
$scope.recipe.reviews = [{}];
}
$scope.calculateAverage = function(AverageData){
var sum = 0;
if ($scope.recipe.reviews.length > 1) {
for(var i = 1; i < $scope.recipe.reviews.length; i++){
sum += parseInt($scope.recipe.reviews[i].stars, 10);
}
var average = sum/($scope.recipe.reviews.length-1);
var roundedAverage = Math.round(average);
return {
average: roundedAverage,
markedStars: new Array(roundedAverage)
};
} else {
return sum;
}
};
});
]);
In your RecipeController definition, you immediately call:
$scope.recipes.$loaded().then(function(payload) { ... }
...assuming that $scope.recipes is defined and has a property of $loaded -- which is not the case.
In your test:
describe("$scope.calculateAverage", function() {
it("calculates average correctly", function() {
var $scope = {};
var controller = $controller('RecipeController', { $scope: $scope });
$scope.calculateAverage();
expect(average).toBe(sum/(Recipes.reviews.length-1));
});
});
...you define scope as an empty object, then inject it into your controller.
Assuming you are using Jasmine as a test framework, you could create a spy like this:
var $scope = {
recipes: {
$loaded: function() { /* this is a mock function */ }
}
};
var deferred = $q.defer();
deferred.resolve({
/* this is the data you expect back from $scope.recipes.$loaded */
});
var promise = deferred.promise;
spyOn($scope.recipes, '$loaded').and.returnValue(promise);
This is just one of many ways you could stub out that function and control the data you get in your test. It assumes a basic understanding of the $q service and the Promise API.
Best Practices
It is best not to attach data to the $scope service. I would recommend reading up on the controllerAs syntax, if you're not familiar with it.
TL;DR: A controller is just a JavaScript "class", and the definition function is its constructor. Use var vm = this; and then attach variables to the instance reference vm (as in "view model", or whatever you want to call it) instead.
Rather than relying on $scope.recipes to have been defined elsewhere, you should explicitly define it in your controller. If recipes are defined in another controller, create a service that both controllers can share.
We have few methods in Angular Controller, which are not on the scope variable.
Does anyone know, how we can execute or call those methods inside Jasmine tests?
Here is the main code.
var testController = TestModule.controller('testController', function($scope, testService)
{
function handleSuccessOfAPI(data) {
if (angular.isObject(data))
{
$scope.testData = data;
}
}
function handleFailureOfAPI(status) {
console.log("handleFailureOfAPIexecuted :: status :: "+status);
}
// this is controller initialize function.
function init() {
$scope.testData = null;
// partial URL
$scope.strPartialTestURL = "partials/testView.html;
// send test http request
testService.getTestDataFromServer('testURI', handleSuccessOfAPI, handleFailureOfAPI);
}
init();
}
Now in my jasmine test, we are passing "handleSuccessOfAPI" and "handleFailureOfAPI" method, but these are undefined.
Here is jasmine test code.
describe('Unit Test :: Test Controller', function() {
var scope;
var testController;
var httpBackend;
var testService;
beforeEach( function() {
module('test-angular-angular');
inject(function($httpBackend, _testService_, $controller, $rootScope) {
httpBackend = $httpBackend;
testService= _testService_;
scope = $rootScope.$new();
testController= $controller('testController', { $scope: scope, testService: testService});
});
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
httpBackend.verifyNoOutstandingRequest();
});
it('Test controller data', function (){
var URL = 'test server url';
// set up some data for the http call to return and test later.
var returnData = { excited: true };
// create expectation
httpBackend.expectGET(URL ).respond(200, returnData);
// make the call.
testService.getTestDataFromServer(URL , handleSuccessOfAPI, handleFailureOfAPI);
$scope.$apply(function() {
$scope.runTest();
});
// flush the backend to "execute" the request to do the expectedGET assertion.
httpBackend.flush();
// check the result.
// (after Angular 1.2.5: be sure to use `toEqual` and not `toBe`
// as the object will be a copy and not the same instance.)
expect(scope.testData ).not.toBe(null);
});
});
I know this is an old case but here is the solution I am using.
Use the 'this' of your controller
.controller('newController',['$scope',function($scope){
var $this = this;
$this.testMe = function(val){
$scope.myVal = parseInt(val)+1;
}
}]);
Here is the test:
describe('newDir', function(){
var svc,
$rootScope,
$scope,
$controller,
ctrl;
beforeEach(function () {
module('myMod');
});
beforeEach(function () {
inject(function ( _$controller_,_$rootScope_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
$compile = _$compile_;
$scope = $rootScope.$new();
ctrl = $controller('newController', {'$rootScope': $rootScope, '$scope': $scope });
});
});
it('testMe inc number', function() {
ctrl.testMe(10)
expect($scope.myVal).toEqual(11);
});
});
Full Code Example
As is you won't have access to those functions. When you define a named JS function it's the same as if you were to say
var handleSuccessOfAPI = function(){};
In which case it would be pretty clear to see that the var is only in the scope within the block and there is no external reference to it from the wrapping controller.
Any function which could be called discretely (and therefore tested) will be available on the $scope of the controller.