How to test AngularJS directives - angularjs

I am working on a Rails 3.2 app that will be using AngularJS. I can get Angular to do what I need, but I am having a very difficult time figuring out how to test what I'm doing. I am using guard-jasmine to run Jasmine specs using PhantomJS.
Here is the (relevant) html:
<html id="ng-app" ng-app="app">
<div id="directive-element" class="directive-element">
</div>
</html>
The javascript (in coffeescript) looks like:
window.Project =
App: angular.module('app', [])
Directive: {}
Project.Directive.DirectiveElement =
->
restrict: 'C'
link: (scope, element, attrs) ->
element.html 'hello world'
Project.App.directive 'directiveElement', Project.Directive.DirectiveElement
The code above does exactly what it is intended to do. The tests are the problem. I can't get them to work at all. This is one thing I had tried. Posting this is mostly just to start the conversation somewhere.
describe 'App.Directive.DirectiveElement', ->
it 'updates directive-element', ->
inject ($compile, $rootScope) ->
element = $compile('<div id="app" ng-app="app"><div id="directive'element" class="directive-element"></div></div>')
expect(element.text()).toEqual('hello world')
As an aside, I am new to AngularJS, so if there are any best practices regarding namespacing, modules, etc. that I am not following, guidance would be appreciated.
How do I get a test for this to work?

Here's how alert directive is tested in angular-ui/bootstrap.
Here's another simple set of tests, for the buttons directive.
Here are a few tips:
Be sure to tell the test runner what module you are testing with beforeEach(module('myModule')).
If you have external templateUrls in your directives, you'll want to somehow pre-cache them for the test runner. The test runner can't asynchronously GET templates. In bootstrap, we inject the templates into the javascript with a build step, and make each template a module. We use grunt-html2js grunt task.
In your tests, use the inject helper in a beforeEach to inject $compile and $rootScope and any other services you'll need. Use var myScope = $rootScope.$new() to create a fresh scope for each test. You can do var myElement = $compile('<my-directive></my-directive>')(myScope); to create an instance of your directive, and have access to its element.
If a directive creates its own scope and you want to test against it, you can get access to that directive's scope by doing var directiveScope = myElement.children().scope() - It will get the element's child (the directive itself), and get the scope for that.
For testing timeouts, you can use $timeout.flush() to end all pending timeouts.
For testing promises, remember that when you resolve a promise, it will not call its then callbacks until the next digest. So in tests you have to do this a lot: deferred.resolve(); scope.$apply();.
You can find tests for directives of varying complexity in the bootstrap repo. Just look in src/{directiveName}/test/.

Angular Test Patterns may help you, there are examples in both coffeescript and javascript.
Here's a testing pattern to verify the example directive is rendering the expected output.

Related

Mocking Module Dependencies in Karma/Jasmine for AngularJS

I'm trying to compose some unit tests in Karma/Jasmine for a particular module in my project, destination-filters.
Module Decleration:
angular.module('destination-filter', ['ngSanitize']);
My tests fail unless I remove ngSanitize as a dependency. To my understanding that is because when the module is instantiated it will try and pull in that dependency but because in my spec.js file I haven't declared that module it is failing.
Spec File:
describe('Destination Filter Controller', function () {
// Set the variables
var $controller;
var mockNgSanitize;
beforeEach(module('destination-filter'));
beforeEach(function() {
module(function($provide) {
$provide.value('ngSanitize', mockNgSanitize);
});
});
beforeEach(inject(function (_$controller_) {
$controller = _$controller_('DestinationFilterController');
}));
it('should expect the controller to not be null', function() {
// Check the controller is set
expect($controller).not.toBeNull();
});
});
Previously, when mocking out services or functions, the $provide method has proven very useful but I'm not sure my use of it is correct here. I'm assuming $provide used in this way can't mock entire modules but rather services?
To clarify, if I remove the ...['ngSantize'])... from my module deceleration the tests instantiate correctly. The error I am receiving with it left in is Error: [$injector:modulerr] destination-filter
There are three options you could take for using ngSanitize in your tests:
inject the service into your test
stub a method call on ngSanitize
mock the entire ngSanitize service
The option you choose is really dependent on the use of ngSanitize in your working code (not your test code).
Whichever one you go for you need to make the service available in your test, there is no need for $provider (this covers option 1 and there is no need to do any more than this if you just want to make this available to your filter):
beforeEach(module('ngSanitize'));
beforeEach(inject(function(_ngSanitize_) { // the underscores are needed
mockNgSanitize = _ngSanitize_;
}));
Also, make sure that all js files are picked up and loaded by karma. You can define this in karma.conf.js by adding them to the files: property.
2. Stub a method on the service
I like stubs and find them very useful when writing tests. Your tests should only test one thing, in your case a filter. Stubs give you more control over your tests and allow you to isolate the thing under test.
Typically filters, controllers, anything call on lots of other things (services or factories like $http or ngSanitize).
Assuming that your filter is using ngSanitize's $sanitize to sanitize some html you could stub out that method to return sanitized html you have defined to test against your expectations:
// in a beforeEach
spyOn(mockNgSanitize, "$sanitize").and.returnValue('<some>sanitized<html>');
mockNgSanitized.$sanitize(yourDirtyHtml);
See the jasmine docs for more info.
You might have to play around with spying on the right service but this should work ok.
3. Mock the entire service
I don't think you want to go with this option because it will drive you insane figuring out what needs mocking plus mocks can create unrealistic expectations and also, not particularly useful for your use case. If you really want to have a go then something like the below is heading in the right direction (again see the jasmine docs)
beforeEach(function() {
mockNgSanitize = ('ngSanitize', ['linky', '$sanitize', '$sanitizeProvider'];
});
it('mocks the ngSanitize service', function() {
expect(mockNgSanitize.linky).toBeDefined();
});
NB: in all the code above make sure you continue to declare any variables up at the top of your describe block.

Trouble getting jasmine working with angular

I'm having trouble understanding how to set up Jasmine to work with Angular so I can do testing. I'm following the instructions here under the heading titled "Testing a controller". According to the documentation, you should have your app & controller, defined like you normally would (this is pasted from the documenation):
angular.module('app', [])
.controller('PasswordController', function PasswordController($scope) {
//controller code goes here (removed for brevity)
});
and then you should have as your testing suite code, for example (pasted from the documentation as well).
describe('PasswordController', function() {
beforeEach(module('app'));
var $controller;
beforeEach(inject(function(_$controller_){
// The injector unwraps the underscores (_) from around the parameter names when matching
$controller = _$controller_;
}));
describe('$scope.grade', function() {
it('sets the strength to "strong" if the password length is >8 chars', function() {
var $scope = {};
var controller = $controller('PasswordController', { $scope: $scope });
$scope.password = 'longerthaneightchars';
$scope.grade();
expect($scope.strength).toEqual('strong');
});
});
});
But I'm terribly confused about a few things.
The documentation explains that you need to use angular-mocks to load in the controller, but in their example, they don't declare ngMocks as an app dependency (see the first block of code I pasted above).
It says that you can use angular.mock.inject to inject the controller into the current context. I loaded in the script http://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular-mocks.js and now there is an angular.mock on the global scope, but it does not have an inject method. Furthermore, since the testing code runs outside of the controller, I don't understand how using the ngMocks dependency in the angular app helps with providing global methods for injecting controllers. The whole thing doesn't make sense to me.
Same goes for module. It says you can use it for the beforeEach(module('app'));, and that it's provide by angular-mocks, but angular.mock has no module method.
If someone could explain what I'm doing wrong I would very much appreciate it!
So I discovered that problem was that my script tag for angular-mocks was before my script tags for Jasmine when it really needs to go after. In the typical spirit of Angular "documentation", this was mentioned nowhere. After rearranging the script tags both module and inject were globally available methods.
So to answer my first question, you don't need to put ngMock in the dependencies. This answers questions 2 and 3 since module and inject are both now globally available.
So the scripts need to be placed in this order.
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.3.4/jasmine.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.3.4/jasmine.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.3.4/jasmine-html.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.3.4/boot.js"></script>
<!--angluar mocks script MUST go after the other declarations otherwise it won't add the inject and module methods to the scope -->
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular-mocks.js"></script>

AngularJS unit test check if function without methods have been called

I'm using Jasmine to unit test my Angular app. It's pretty easy to test if a method of a function has been called using something like:
spyOn($rootScope, "$emit");
expect($rootScope.$emit).toHaveBeenCalled();
But I can't find a way to check when a function has been called (without a method), for e.g. I'm using $anchorScroll(); in one controller and I have no idea where to apply the above code to this guy. I've seen some Jasmine examples where they were using expect(window.myFunction()).toHaveBeenCalled(), but this doesn't work with Angular's DI.
I can't try it myself at the minute but maybe you could just inject a mock $anchorScroll instead?
var $anchorScroll = jasmine.createSpy('anchorScroll');
$controller('MyCtrl', {
$anchorScroll: $anchorScroll
});
expect($anchorScroll).toHaveBeenCalled();
This should just create a blank spy, one which will take any arguments and do nothing but keep track of the calls for test usage.

AngularJS how to unit test to ensure directives like ng-click point to valid code

I'm using Jasmine to write unit tests for our controllers, but wanted to get community feedback on how to handle this situation...
I have a controller - InvoiceController, like this:
angular.module('myModule').controller('myController', ['$scope',
function($scope) {
$scope.doSomething = function() {
$scope.something = 'bar';
};
}
]});
In my unit tests I verify that my controller has the expected methods:
it('should be able to do some work', function() {
// initialize scope properties
scope.someProperty = 'foo';
// load controller using those properties
injectController();
// do I have all of the functions necessary to do this work?
expect(typeof (scope.doSomething)).toBe('function');
// now execute test
scope.doSomething();
expect(scope.something).toBe('bar');
}
And finally, in my html I have an element with an ng-click, like this:
<button ng-click="doSomehing()">Do Something</button>
Looks good, right? BUT, did anyone catch what I did wrong?
My ng-click method is misspelled, but all tests are green and life seems rosy...until I try to click on that guy and nothing happens. No render time error, no error on click. Hmm.
Several times now as I'm refactoring code this has got me. I rename doSomething to doSomethingCooler in the unit test and in the controller but miss a place in the html. After a minute of head scratching I see what was missed.
I'd love a way to ensure that the markup is valid. E2E tests seem to be the obvious solution, but those are prone to fragility so we are hoping there are some alternatives.
If this were ASP.Net I would attach the click events from code behind so that I would get compile time errors vs run time errors.
Thoughts??
Thad
One thing you could do is get the template text and run $compile on it. And then link it to your controller's scope. Then you could do something like dom.find('[ng-click]').click();, which should throw if any of them is not defined in your controller's scope. This is similar to how you generally test directives.

Angular testing, using mocha, unexpected requests

In my angular tests I keep getting a Error: Unexpected request: GET 'some/rails/view.html'
I'm using konacha for testing, which uses mocha instead of jasmine. The project is based around a Rails app which is the reason for using konacha.
Here's a really simple sample test that checks if the controller is defined in the Angular app:
describe "ShowAccountCtrl", ->
beforeEach angular.mock.module('APP_NAME')
beforeEach inject(($rootScope, $controller) ->
#scope = $rootScope.$new()
#ctrl = $controller 'ShowAccountCtrl',
$scope: #scope
)
it "should be defined", ->
expect(#ctrl).to.not.be.undefined
I've seen some things about $httpBackend.when('GET', /\.html$/).passThrough(); but konacha doesn't seem to have a similar method to passThrough()
These issues always happen upon a $httpBackend.flush().
Has anyone conquered this problem before? Is there a way to ignore requests to rails templates so I can focus on testing functionality of the controllers?
This is because Konacha doesn't support any integration with Rails views. The solution is to load angular's $templateCache manually, similar to what you have to do when using templates with the asset pipeline. To make this work you will need to make your test have the ERB pre-processor (e.g. some_spec.js.coffee.erb).
beforeEach inject ($templateCache) ->
template = '/app/templates/view.html'
content = """
<%= IO.read(Rails.root.join('/app/templates/view.html')) %>
"""
$templateCache.put template, content

Resources