Unit-testing secondary routing controller - angularjs

Currently i'm developing a simple pizza-webshop for a school assignment. I'm experienced in Java & C# but AngularJS is new to me.
I created a single page application with 1 main order controller (index contains an overview of the total order) and another menu controller to load the .JSON menu into a partial view.
The order controller is set in the index as follows;
index.html
<html ng-app="pizzaApp" ng-controller="orderCtrl">
<head>
<link rel="stylesheet" href="CSS/style.css"/>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular-route.min.js"></script>
<script src="Apps/pizzaApp.js"></script>
<script src="Controllers/menuCtrl.js"></script>
<script src="Controllers/orderCtrl.js"></script>
</head>
and the menu controller is set in the app's routing as follows;
pizzaApp.js
var app = angular.module("pizzaApp", ["ngRoute"]);
// View routing
app.config(function ($routeProvider) {
$routeProvider
.when('/', {
templateUrl : 'Views/main.html'
})
.when('/pizzas', {
templateUrl: 'Views/pizzas.html',
controller: 'menuCtrl'
})
.when('/pizzas/:param', {
templateUrl: 'Views/pizzaInfo.html',
controller: 'menuCtrl'
})
.when('/orders', {
templateUrl: 'Views/orders.html',
})
});
I managed to write a working (and passing) unit-test for the order controller as follows;
orderCtrl.spec.js
describe('Controllers', function() {
beforeEach(module('pizzaApp'));
var $controller;
beforeEach(inject(function(_$controller_){
$controller = _$controller_;
}));
describe('orderCtrl Test', function() {
var $scope, controller;
beforeEach(function() {
$scope = {};
controller = $controller('orderCtrl', { $scope: $scope });
});
it('orderCtrl.addPizza methode', function() {
//code...
});
it('orderCtrl.clearList methode', function() {
//code...
});
});
});
However, if i try to write a simple unit test for the menu controller in the exact same way, it says the scope/controller is undefined and/or gives me back nulls resulting in the following error message;
Expected null to equal [ ].
at UserContext.<anonymous> (C:/Users/Ken/Git repositories/angular-phonecat/app/practicum/Unit_tests/menuCtrl.spec.js:88:34)
Error: Unexpected request: GET Models/menu.json
Expected GET ../Models/menu.json
menuCtrl.spec.js
describe('menuCtrl Tests', function() {
beforeEach(module('pizzaApp'));
var $controller, $httpBackend;
beforeEach(inject(function(_$controller_, _$httpBackend_){
$controller = _$controller_;
$httpBackend = _$httpBackend_;
$httpBackend.expectGET("../Models/menu.json").respond([{name: 'Margaritha'}, {name: 'Shoarma'}, {name: 'Supreme'}]);
}));
describe('menuCtrl Tests', function() {
var $scope, controller;
beforeEach(function() {
$scope = {};
controller = $controller('menuCtrl', { $scope: $scope});
});
it('should create a `menu` property with 3 pizzas with `$httpBackend`', function() {
jasmine.addCustomEqualityTester(angular.equals);
expect($scope.Menu).toEqual([]);
$httpBackend.flush();
expect($scope.Menu).toEqual([{name: 'Margaritha'}, {name: 'Shoarma'}, {name: 'Supreme'}]);
});
});
});
menuCtrl.js
app.controller('menuCtrl', function ($scope, $http, $routeParams) {
$scope.param = $routeParams.param; //URL Parameter (.../index.html#!/pizzas/#)
$scope.test="123";
//Menu array inladen vanuit .JSON
$http.get('Models/menu.json')
.then(function (menu) {
$scope.Menu = menu.data;
});
});
I have read the f*cking manual, searched google for many-many hours, read all similar questions on stack and i have been trying to solve this for about 2 weeks total, all to no avail...
I have tried changing the unit tests, controllers, front-end, routing, dependency injections, using controller instead of scope, etc. etc. ... and i am COMPLETELY stuck!

In menuCtrl.js, add:
$scope.Menu = [];
before the GET request, i.e. after
$scope.test="123";
The test is expecting a scope variable Menu to exist and since it is not initialized until after the GET request, the test is not passing.
Also a side note,
In the test, you are expecting a GET request to the URI "../Models/menu.json" (in $httpBackend.expectGET()) and making the GET request to the URI "Models/menu.json". If this is unintentional, you may need to change either of those values

Related

Stubbing factory being used in resolve

I'm trying to unit test my states in an controller. What I want to do is stub out my items factory, since I have separate unit tests that cover that functionality. I'm having a hard time getting the $injector to actually inject the factory, but it seems like I'm letting the $provider know that I want to use my fake items object when it instantiates the controller. As a disclaimer I'm brand new to angular and would love some advice if my code looks bad.
Currently when I run the test I get the message:
Error: Unexpected request: GET /home.html
No more request expected
at $httpBackend (node_modules/angular-mocks/angular-mocks.js:1418:9)
at n (node_modules/angular/angular.min.js:99:53)
at node_modules/angular/angular.min.js:96:262
at node_modules/angular/angular.min.js:131:20
at m.$eval (node_modules/angular/angular.min.js:145:347)
at m.$digest (node_modules/angular/angular.min.js:142:420)
at Object.<anonymous> (spec/states/homeSpec.js:29:16)
It appears that my mocked items factory isn't being injected into the test. When I place a console.log line in the method I want to stub in the items factory I see that line being invoked.
The code I'm looking to test is as follows:
angular.module('todo', ['ui.router'])
// this is the factory i want to stub out...
.factory('items', ['$http', function($http){
var itemsFactory = {};
itemsFactory.getAll = function() {
// ...specifically this method
};
return itemsFactory;
}])
.controller('TodoCtrl', ['$scope', 'items', function($scope, items) {
// Do things
}])
.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider){
$stateProvider
.state('home', {
url: '/home',
templateUrl: '/home.html',
controller: 'TodoCtrl',
resolve: {
items: ['items', function(items){
// this is the invocation that i want to use my stubbed method
return items.getAll();
}]
}
});
$urlRouterProvider.otherwise('home');
}]);
My test looks like this:
describe('home state', function() {
var $rootScope, $state, $injector, state = 'home';
var getAllStub = sinon.stub();
var items = {
getAll: getAllStub
};
beforeEach(function() {
module('todo', function($provide) {
$provide.value('items', items);
});
inject(function(_$rootScope_, _$state_, _$injector_) {
$rootScope = _$rootScope_;
$state = _$state_;
$injector = _$injector_;
});
});
it('should resolve items', function() {
getAllStub.returns('getAll');
$state.go(state);
$rootScope.$digest();
expect($state.current.name).toBe(state);
expect($injector.invoke($state.current.resolve.items)).toBe('findAll');
});
});
Thanks in advance for your help!
Allowing real router in unit tests is a bad idea because it breaks the isolation and adds more moving parts. I personally consider $stateProvider, etc. stubs a better testing strategy.
The order matters in config blocks, service providers should be mocked before they will be injected in other modules. If the original modules have config blocks that override mocked service providers, the modules should be stubbed:
beforeAll(function () {
angular.module('ui.router', []);
});
beforeEach(function () {
var $stateProviderMock = {
state: sinon.stub().returnsThis()
};
module(function($provide) {
$provide.constant('$stateProvider', $stateProviderMock);
});
module('todo');
});
You just need to make sure that $stateProvider.state is called with expected configuration objects an arguments:
it('should define home state', function () {
expect($stateProviderMock.state.callCount).to.equal(1);
let [homeStateName, homeStateObj] = $stateProviderMock.state.getCall(0).args;
expect(homeStateName).to.equal('home');
expect(homeState).to.be.an('object');
expect(homeState.resolve).to.be.an('object');
expect(homeState.resolve.items).to.be.an('array');
let resolvedItems = $injector.invoke(homeState.resolve.items);
expect(items.getAll).to.have.been.calledOnce;
expect(resolvedItems).to.equal('getAll');
...
});

Missing compiled element contents when unit testing ngView

After reading the angular ngViewSpec.js file, I've finally figured how to test my controller with routes, controllerAs and forms. Here is my unit test:
describe("mainControllerSpec", function() {
var element;
beforeEach(module("nisArch"));
beforeEach(module("ngRoute"));
it("clickSearch should change location with limitResults true", function () {
var path, search;
var ctrl;
module(function () {
return function ($rootScope, $compile) {
element = $compile("<div ng-view></div>")($rootScope);
};
});
module(function ($compileProvider, $routeProvider) {
$routeProvider.when("/", {
title: "Home",
controller: getController,
controllerAs: "vm",
templateUrl: "templates/mainView.html"
});
});
inject(function ($location, $controller, $rootScope, $route, $templateCache) {
ctrl = $controller("mainController", { $location: $location });
$location.path("/");
$rootScope.$digest();
//breakpoint here. element.html() == "".
//Expected contents of $templateCache.get(...)
ctrl.queryString = "AS65402";
ctrl.clickSearch();
path = $location.path();
search = $location.search();
expect(path).toEqual("/Search");
expect(search).toEqual({ q: "AS65402", limitResults: "true" });
});
function getController() {
return ctrl;
}
});
});
If I understand correctly what's going on here:
I create an element with my ng-view directive, compile it, bind it to the rootscope. I then create the route and controller. Finally I set the $location and do the main part of my unit test.
This is working fine and my test is passing. What's puzzling me is that I can't see the contents of element. I would have expected them to be the contents of templates/mainView.html after the router change and $digest(). But I get nothing. I injected $route to check $route.current.templateHtml and I injected $templateCache to check that. Both correct.
Should I not expect to find the contents using element.html()?
Or have I completely misunderstood what's going on here?
Thanks for any help or insight,
Marcus
It turns out that the magic is in element.siblings(). I got the first clue with element[0]:
<!-- ngView: -->
Which explains why element.html() and element.text() were undefined
Everything I was looking for is under element.siblings()[0]:
<div class="ng-scope" ng-view="">...</div>
I found the element[0] trick from:
https://learn.jquery.com/using-jquery-core/faq/how-do-i-pull-a-native-dom-element-from-a-jquery-object/
Hope this helps someone,
Marcus
In my case I had to use ngView[0].nextSibling here is my test
it 'check view content', ->
inject ($route, $location, $rootScope, $httpBackend, $compile)->
ngView = $compile("<div ng-view></div>")($rootScope)
using $httpBackend, ->
#.expectGET(url_Template).respond(html)
#.expectGET(url_Data ).respond test_Data
$location.path(url)
$httpBackend.flush()
element = ngView[0].nextSibling
$scope = angular.element(element).scope()
$scope.team.assert_Is 'team-A'
$scope.table.assert_Is metadata: 42

AngularJS: unit testing ui-router state changes

I am fairly new to unit testing in angular so bear with me please!
I have a $stateProvidor setup for my app and would like to test that the routing part does work correctly.
Say I have this sort of config:
angular.module("app.routing.config", []).config(function($stateProvider, $urlRouterProvider) {
$stateProvider.state("home", {
url: "/",
templateUrl: "app/modules/home/page/home_page.html",
controller: "HomePageController",
resolve: {
setPageTitle: function($rootScope) {
return $rootScope.pageTitle = "Home";
}
}
}).state("somethingelse", {
url: "/",
templateUrl: "app/modules/home/page/somethingelse.html",
controller: "SomeThingElseController",
resolve: {
setPageTitle: function($rootScope) {
return $rootScope.pageTitle = "Some Thing Else";
}
}
});
return $urlRouterProvider.otherwise('/');
});
I came across this blog post on how to set up unit testing for a ui-router config, so I Have tried to adopt the same approach, here is my test I am trying out:
'use strict';
describe('UI-Router State Change Tests', function() {
var $location, $rootScope, $scope, $state, $templateCache;
beforeEach(module('app'));
beforeEach(inject(function(_$rootScope_, _$state_, _$templateCache_, _$location_) {
$rootScope = _$rootScope_;
$state = _$state_;
$templateCache = _$templateCache_;
$location = _$location_;
}));
describe('State Change: home', function() {
beforeEach(function() {
$templateCache.put(null, 'app/modules/home/page/home_page.html');
});
it('should go to the home state', function() {
$location.url('home');
$rootScope.$digest();
expect($state.href('home')).toEqual('#/');
expect($rootScope.pageTitle).toEqual('Home');
});
});
});
When running the test I am getting this error in the output:
Error: Unexpected request: GET app/modules/home/page/home_page.html
Clearly I am doing something wrong here, so any help or pointers would be much appreciated.
I did come across $httpBackend, is this something I should also be using here, so telling my test to expect a request to the html page my state change test is making?
This is almost certainly down to a partial html view (home_page.html) being loaded asynchronously during app / test runtime.
In order to handle this, you can preprocess your html partials into Javascript strings, which can then be loaded synchronously via your tests.
Have a look at karma-ng-html2js-preprocessor which should solve your problem.

Unit testing controller which uses $state.transitionTo

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

Simple Angular $routeProvider resolve test. What is wrong with this code?

I have created a simple Angular JS $routeProvider resolve test application. It gives the following error:
Error: Unknown provider: dataProvider <- data
I would appreciate it if someone could identify where I have gone wrong.
index.html
<!DOCTYPE html>
<html ng-app="ResolveTest">
<head>
<title>Resolve Test</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular.js"> </script>
<script src="ResolveTest.js"></script>
</head>
<body ng-controller="ResolveCtrl">
<div ng-view></div>
</body>
</html>
ResolveTest.js
var rt = angular.module("ResolveTest",[]);
rt.config(["$routeProvider",function($routeProvider)
{
$routeProvider.when("/",{
templateUrl: "rt.html",
controller: "ResolveCtrl",
resolve: {
data: ["$q","$timeout",function($q,$timeout)
{
var deferred = $q.defer();
$timeout(function()
{
deferred.resolve("my data value");
},2000);
return deferred.promise;
}]
}
});
}]);
rt.controller("ResolveCtrl",["$scope","data",function($scope,data)
{
console.log("data : " + data);
$scope.data = data;
}]);
rt.html
<span>{{data}}</span>
The problem is that you have ng-controller="ResolveCtrl" on your body tag in index.html when also in your $routeProvider you specify the same controller for rt.html. Take out the controller definition from your body tag and just let the $routeProvider take care of it. It works great after that.
According to the angularjs documentation for $routeprovider the resolve object is a map from key (dependency name) to factory function or name of an existing service. Try this instead:
var myFactory = function($q, $timeout) { ... };
myFactory.$inject = ['$q', '$timeout'];
$routeProvider.when("/",{
templateUrl: "rt.html",
controller: "ResolveCtrl",
resolve: {
data: myFactory
}
});
By adding data to the definition of the controller your telling angular that you expect to inject a service or factory here yet you don't have a data service or factory thus the error. To use the data variable you have all you need from the $scope.data line. So to fix this you need to remove the data injection from your controller call.
var rt = angular.module("ResolveTest",[]);
rt.config(["$routeProvider",function($routeProvider)
{
$routeProvider.when("/",{
templateUrl: "rt.html",
controller: "ResolveCtrl",
resolve: {
data: ["$q","$timeout",function($q,$timeout)
{
var deferred = $q.defer();
$timeout(function()
{
deferred.resolve("my data value");
},2000);
return deferred.promise;
}]
}
});
}]);
rt.controller("ResolveCtrl",["$scope", function($scope)
{
$scope.data = "";
}]);
If you want to have a data provider add a factory something like
rt.factory('data', ['$http', function($http){
return {
// Functions to get data here
}
}]);
Then in your controller call the appropriate function from this factory.
Also as the others have pointed out you don't need the controller both in your route and in an ng-controller (this will nest your controller in your controller if you inspect the scopes).
If you must use resolve you still need a factory as resolve will just point to the proper factory which needs to be declared separately.

Resources