AngularJS Unit testing $location - unit-testing

I am pretty new at unit testing and AngularJS and I have some issue that I can't fix. One of my test is not working. I am trying to initiate a location.path() in my test by affecting a value, but in my controller, location.path() still have a undefined value.
Here is my controler:
angular.module('...')
.controller('SignUpCtrl', ['$location', function ($location) {
// Retrieve type of user
var userType = $location.path().substr(9);
if(userType == 'member'){
userType = 'user';
}
console.log($location.path());
console.log(userType);
$scope.uType = userType; ]);
And here is my test module:
describe('Controller: SignUpCtrl', function () {
// load the controller's module
beforeEach(module('...'));
var SignUpCtrl,
scope,
mockBackend,
environments,
location,
store;
beforeEach(inject(function ($controller, $rootScope, $httpBackend,$location,_Environments_) {
environments = _Environments_;
mockBackend = $httpBackend;
location = $location;
scope = $rootScope.$new();
SignUpCtrl = $controller('SignUpCtrl', {
$scope: scope,
$location: location
});
}));
it('should come from the right location', function(){
location.path('/sign-up/member');
expect(location.path()).toBe('/sign-up/member');
expect(scope.uType).toBe('user'); //Do not work
});
});

You're trying to use unit testing to do something that can only really be achieved using End-to-End (or E2E) testing. Unit testing in AngularJS is designed to test the javascript within a given module or sub-module (such as a service, factory, directive, etc). However, things like page navigation or browser location really need to be tested in an end-to-end testing environment.
Because of that, your $location object won't have all the normal methods (like path, url, etc). The $location object ends up simply being a "mock" of the actual $location object that you'd get in your module. So, you just need to move your test case for it('should come from the right location', function(){ ... }) to an end-to-end test and then continue on with your other module-specific unit tests. After you do that, you can simplify the $controller by only grabbing the $scope variable, as in the following:
scope = $rootScope.new();
SignUpCtrl = $controller('SignUpCtrl', {$scope: scope});
The guide for E2E testing can be found at this link. It walks you through how to write good E2E tests. There is a really great framework available for doing angular E2E tests called Protractor. The info for that is at this link. Protractor will soon (in 1.2) replace Karma as a better way to handle E2E testing.

Related

Assert inside of a stub/mock ember service during acceptance/integration tests

In unit tests for a service, I have been putting asserts inside of service stubs, which has come in rather handy.
unit-test.js
let fooServiceStub = Ember.Object.extend({
fooMethod(bar) {
this.assert.ok(bar, 'fooMethod called with bar');
}
});
...
test('blah', function(assert) {
assert.expect(1);
let stubFooService = fooServiceStub.create({ assert });
let fooService = this.subject({
fooService: stubFooService
});
fooService.fooMethod('data');
});
Is an assert inside of a stub service possible for an acceptance/integration test?
The issue that I am running into is that for acceptance/integration tests, the way the service is injected is different from unit tests.
acceptance-test.js
let fooServiceStub = Ember.Service.extend({
fooMethod(bar) {
return 'baz';
}
});
....
beforeEach: function () {
this.application.register('service:mockFooService', fooServiceStub);
this.application.inject('controller', 'fooService', 'service:mockFooService');
}
I have not found a way to pass in the 'assert' object into such a stub.
To me, this is desirable to do during an acceptance test. The service goes off and does stuff that would be rather complicated to mock in the acceptance test, and I don't want to re-test my service. I just want to confirm the expected service calls were triggered.
You can just do something like this in your test:
this.set('fooService.FooMethod', bar => assert.ok(bar, 'bla'));

Test Angular controller with Protractor

This is example of my code:
describe('myCtrl functionality', function() {
var driver;
var ptor;
beforeEach(function() {
ptor = protractor.getInstance();
ptor.ignoreSynchronization = true;
browser.ignoreSynchronization = false;
driver = ptor.driver;
});
it('should login', function() {
driver.get('someurl');
driver.findElement(protractor.By.name('username')).sendKeys('admin');
driver.findElement(protractor.By.name('password')).sendKeys('admin');
driver.findElement(protractor.By.css('button[type="submit"]')).click();
});
describe('myCtrl testing', function() {
var $scope;
beforeEach(module('myApp'));
beforeEach(inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
$controller('dashStoresCtrl', {$scope: $scope});
}));
it('should create "stores" model', function() {
var containerStores = element(by.css('.dashboardStores'));
containerStores.findElements(by.css('.store-item-holder')).then(function(elems) {
expect(elems.length).toEqual($scope.stores.length);
});
});
});
});
And the problem is when i run tests i get TypeError: object is not a function.
That is for the line beforeEach(module('myApp'));
I made research and find out that i need to include angular-mocks.js file in my project and in index.html.
I did it but still get TypeError: object is not a function.
Anyone who can help with this?
Thanks!!!
Protractor tests are end-to-end tests, where NodeJS executes tests that connect to your browser and use it like a numan being would do.
You're trying, in such a protractor test, to use the angularJS API and modules to unit-test a controller. That doesn't make much sense.
Unit tests are typically executed by Karma, inside your browser, and end-to-end protractor tests are typically executed using protractor, inside NodeJS. You shouldn't have a unit test and a protractor test in the same file.

how to mock a location.path in angular unit tests

http://blog.artlogic.com/2013/05/06/angularjs-best-practices-ive-been-doing-it-wrong-part-2-of-3/
I am testing a routing directive with location.path refs to templates that cannot be found. So....
I want to mock a routing test with a mock configured $routeProvider, how to get the $routeProvider in karma/jasmine?
then I tried the spyOn mock approach described in best-practice, is there a syntax for which I can expect the $location.path().toBe('/path')?
spyOn($location, 'path').andCallFake(new LocationMock().path);
and I was wondering if I could
I created a fiddle which demonstrates mocking $location.
app.controller('testcont', function($scope, $location) {
$scope.path = $location.path();
});
...
beforeEach(inject(function($controller, $rootScope, $location){
scope = $rootScope.$new();
spyOn($location, 'path').andReturn('Fake location');
$controller('testcont', {$scope:scope});
}));
...
it('should spy on $location', function($location){
expect(scope.path).toBe('Fake location');
});
However templates can be loaded by prepopulating Angular's $templateCache with the directive.
Karma uses the ng-html2js-preprocessor. Could that help your problem?

Mocking AngularJS module dependencies in Jasmine unit tests

I'm attempting to unit test controller code inside a module that takes other modules as dependencies, but haven't been able to figure out how to mock them properly.
I'm using the Jasmine Framework and running my tests with Karma (Testacular).
Module Code
var app = angular.module('events', ['af.widgets', 'angular-table']);
app.controller('eventsCtrl', function([dependencies]){
$scope.events = [];
...
});
Spec Code
describe('events module', function(){
var $scope,
ctrl;
beforeEach(function(){
angular.mock.module('af.widgets', []);
angular.mock.module('angular-table', []);
module('events', ['af.widgets', 'angular-table']);
});
beforeEach(inject(function($rootScope, $controller){
$scope = $rootScope.new();
ctrl = $controller('NameCtrl', {
$scope: $scope,
});
}));
it('should have an empty events array', function(){
expect($scope.events).toBe([]);
})
});
The error I'm getting is Karma is "no module af.widgets", so obviously I'm not mocking the module dependencies right. Any hints?
If you want to mock a module that declare one or more services I have used this code:
beforeEach(function(){
module('moduleToMock');
module(function ($provide) {
$provide.value('yourService', serviceMock);
});
});
This is useful if the service you want to mock is also a service that you want to unit test (in another jasmine describe).
The solution proposed by fscof is fine but you cannot create a unit test for the angular-table module.
Here's what I figured out:
I wasn't loading any 'angular-table' modules in my karma.conf.js file, hence the error. This was intentional at first as I wanted to test the 'events' module without the actual table module.
I was able to easily mock the 'angular-table' module by creating a new file in my test folder called 'mocks/angular-table.js' and added the following code:
/mocks/angular-table.js
'use-strict';
angular.module('angular-table', []);
I added this file to my karma.conf.js file, along with the real 'events' module I wanted to test:
karma.conf.js
...
files = [
JASMINE,
JASMINE_ADAPTER,
'scripts/libs/angular.js',
'scripts/libs/angular-mocks.js',
'scripts/events.js', // this is the real module.
'scripts/mocks/*.js', //loads all custom mocks.
'scripts/specs/*.spec.js' // loads my spec file.
]
...
Finally in my spec file, I was able to add both modules by calling them separately in a beforeEach block:
specs/events.spec.js
beforeEach(function(){
module('angular-table');
module('events');
});
I got the idea to structure my files in this way from this post
I recently released ngImprovedTesting that should make mock testing in AngularJS way easier.
In your case just use the following in your Jasmine test:
beforeEach(ModuleBuilder.forModule('events').serviceWithMocks('eventsCtrl').build());
For more information about ngImprovedTesting check out its introductory blog post: http://blog.jdriven.com/2014/07/ng-improved-testing-mock-testing-for-angularjs-made-easy/

Testing AngularJS controllers with resource services

Background:
I'm writing unit test for angular js controllers, which utilize angular $resources wrapped in services (for maintainability purposes).
Example controller:
name = 'app.controllers.UsersIndexCtrl'
angular.module(name, [])
.controller(name, [
'$scope'
'$location'
'$dialog'
'Users'
'UserRoles'
($scope, $location, $dialog, Users, UserRoles) ->
# Get users list
$scope.users = Users.query()
# rest...
])
Example resource service:
angular.module('app.services.Users', [])
.factory 'Users', ['$rootScope', '$http', '$resource', '$location' , ($rootScope, $http, $resource, $location)->
baseUrl = '/users'
Users = $resource baseUrl + '/:userId', {userId: '#_id'}
Users.getStatus = ->
console.log 'User::getStatus()'
req = $http.get baseUrl + '/status'
req.success (res)->
$rootScope.globalUserAccountSettings = res
unless $rootScope.$$phase then $rootScope.$apply()
# other, custom methods go here...
])
Most of unit test examples in angular suggest using $httpBackend and thus mocking the $http service in controllers. To be honest, I doubt if it's a good practice since if did so I'd have to hardcode request paths in all controller tests and I want to isolate unit behaviour. $httpBackend mock is really great but only if you are using $resource in controllers directly.
A typical single test using $httpBackend would look like:
it 'should be able to add a new empty user profile', ->
$httpBackend.flush()
l = $scope.users.length
$httpBackend.expect('POST', '/users').respond _.cloneDeep mockResponseOK[0]
$scope.add()
$httpBackend.flush()
expect($scope.users.length).toEqual l + 1
What if I created a mock User resource class instance, something like:
angular.module('app.services.Users', [])
.factory 'Users', ->
class Users
$save:(cb)->
$remove:->
#query:->
#get:->
Angular DI mechanisms will override old 'app.services.Users' module with this one in a transparent way and enable me to run checks with jasmine spies.
What bothers me is the fact that I wasn't able to find a single example supporting my idea. So the question is, which one would you use and why or what am I doing wrong?
I think it makes much more sense to stub this at a service level with Jasmine spies, as you suggested. You're unit testing the controller at this point, not the service -- the exact way in which an http request is made should not be a concern for this test.
You can do something in your spec like this:
var Users = jasmine.createSpyObj('UsersStub', ['query', 'get']);
beforeEach(inject(function($provide) {
$provide.factory('Users', function(){
return Users;
});
});
And then in your relevant tests, you can stub the individual service methods to return what you expect using methods like "andCallFake" on your spy object.
The best thing you can do is make a fake resource with the methods that were suppose to be called:
var queryResponse = ['mary', 'joseph'],
Users = function() {
this.query = function() {
return queryResponse;
},
scope, HomeCtrl;
};
beforeEach(inject(function($rootScope, $controller) {
scope = $rootScope.$new();
HomeCtrl = $controller('HomeCtrl', {$scope: scope, Users: new Users()});
}));
it('has users in scope', function() {
expect(scope.users).toEqual(queryResponse);
});
I'm a newbie at this stuff. I've been writing my tests using coffeescript using a dsl, but I've ran into a similar problem today. The way I solved it was by creating a jasmine spy for my resource. Then I created a promise. When the promise is resolved, it will call the 'success' function that you pass in in the controller. Then in the 'it' method, I actually resolve the promise.
I think the code would look something like this using js and jasmine, but I didn't actually have time to check
beforeEach(inject(function($rootScope, $controller, $q ) {
scope = $rootScope.$new();
queryPromise = $q.defer()
User = jasmine.createSpyObject("UsersStub", ["query"]);
User.query.andCallFake(function(success,errror){queryPromise.promise.then(success,error)});
HomeCtrl = $controller('HomeCtrl', {$scope: scope, Users: new Users()});
}));
it('has users in scope', function() {
queryPrmomise.resolve({User1: {name"joe"})
expect(scope.users).toEqual(queryResponse);
});