AngularJS - Testing a directive's link function, how to spy on controller - unit-testing

If I have directive like this
JS:
app.controller('MyController', function($scope) {
this.someMethod = function() {
};
});
app.directive('myDirective', function() {
return {
scope: true
link: function(scope, elem, attrs, controller) {
controller.someMethod();
}
controller: 'MyController',
}
});
I want to create a Jasmine spy to ensure that the link function called controller.someMethod, but this will not work:
Spec:
var elem = angular.element('<div my-directive></div>');
var scope = $rootScope.new();
$compile(elem)(scope);
var ctrl = elem.controller('myDirective');
spyOn(ctrl, 'someFunc').andCallThrough();
The spy is created too late, because the controller was instantiated and the link function called in the $compile statement.
What other ways are there for spying on something that happens in the link function? Is it possible to maybe instantiate the controller before hand and pass it into $compile?

From the AngularJS Developer Guide's page on Directives:
Best Practice: use controller when you want to expose an API to other directives. Otherwise use link.
I would suggest creating a service for whatever someMethod() does. Then you can mock and spy on that service. Otherwise you may have to look for some other sign that what you wanted to happen has actually happened.

Related

ember unit test route - this is undefined

I try to write my first unit test for a route.
project/files
actions: {
afterSave(savedFile){
// ... some code
let controller = this.controllerFor('project.files');
// ...
}
}
the test:
test('save file', function(assert) {
let route = this.subject();
console.log(route);
let project;
Ember.run(() => {
project = route.get('store').createRecord('project', {
id: '1',
name: 'test'
});
let afterSave = route.get('actions.afterSave');
afterSave(project);
});
assert.ok(true);
})
The problem that I am getting TypeError: Cannot read property 'controllerFor' of undefined.
It looks like this is undefined.
If you have a look at Testing Routes section from Ember Guides, you can see its suggestion is to separate the action and the function.
I can suggest it.
It uses send method of routes, such as: route.send('afterSave');
But if you only want to make run your code, call afterSave action from your test code such as: afterSave.bind(route)(project);. Ref: bind function (I don't suggest this. Also I don't suggest you to retrieve action such as: route.get('actions.afterSave'))

Injection of service into Ember tests

I have read and followed EmberJS Service Injection for Unit Tests (Ember QUnit) but I'm still not able to figure where the problem is.
I would like to test if my authentication is working as expected. I have written authenticator for ember-simple-auth and session is injected into route. Code itself is working without any issues.
export default Ember.Route.extend({
authManager: Ember.inject.service('session'),
...
(in actions):
this.get('authManager').invalidate()
Now, I want to create a test which will test if my authentication is working as I expect. So I wish to use authManager directly.
moduleFor('route:index', 'Unit | Route | xyz', {
needs: ['service:session']
});
test('2', function(assert) {
let route = this.subject();
let s = route.get('authManager');
When I print the content of 's', I get ''. If I change this to something else, then response is undefined as can be expected. Problem is when I want to obtain property 'isAuthenticated' or run 'invalidate()'. In these cases I got 'undefined'. What am I doing wrong?
As of Ember 2.13, the correct solution to this is to use this.register:
test('my test', function(assert) {
this.register('service:session', Ember.Service.extend({
/* mock code */
}));
let subject = this.subject();
// test code goes here...
}
In a unit test, we prefer to use mock objects instead of services. In integration tests, we may use real services instead of mocks.
To mock a service, in a unit test:
var stubMyService = Ember.Object.extend({
//This is a mock object, write a code to test component/route!
invalidate: function() {
return 'invalidateCalled';
},
isAuthenticated: function(){
return true;
}
});
To inject this mock object to your component/route use this.subject() creation as following:
test('2', function(assert){
var component = this.subject({
authManager: stubMyService.create()
});
...
});

How can you listen to ember route changes?

How can you detect route changes (# or popstate) outside an ember app? I've tried this but not had any luck: they never fire
window.onhashchange = function locationHashChanged() {
console.log(location.hash);
};
window.addEventListener('popstate', function locationHashChanged(e) {
console.log(e.location);
});
I'm not 100% sure if there's a good way to do this in the public API. But if you're OK with a little trickery, you can use the properties described here.
var applicationController = App.__container__.lookup('controller:application');
applicationController.addObserver('currentRouteName', function() {
var currentRouteName = applicationController.get('currentRouteName');
// ...
});

how to unit test controller which uses this.get('store')

I am unit testing my controller using mocha. My controller looks like:
AS.MyController = Ember.ObjectController.extend(Ember.Validations.Mixin, {
name: null,
description: null,
init: function () {
this._super();
this.get('store').find('something');
},
....
});
And my test begins like:
describe("MyControllerTest", function () {
//tried but didn't work
//delete AS.MyController.init;
var controller = AS.MyController.create();
.....
})
and the browser always throws error on "this.get('store')" call in init. I am not sure if I need to stub things out or there is a work around for it because my test case doesn't rely on store at all. In either case, I couldn't find much out there and would really appreciate any feedback.
Thanks, Dee
JSBIN : http://jsbin.com/aMASeq/3/
UPDATE :
There can be many ways to tackle this issue, but what I ended up doing is re-structuring the controller code a bit by putting all the function calls to store into separate actions and then in init I make calls to these action functions using this.send('actioName'). In my unit test, before instantiating the controller, I reopen the controller to modify these action functions(its easier to change action function than to change init function itself, when trying to change init I always got into some js error). Eg:
AS.MyController.reopen({actions: {setSomeActionThatUsesStore: function () {
//do something that doesn't involve using store
}}});
Controllers get access to the store from the container. You can create a mock container and instantiate the controller with it.
var mockContainer = new Ember.Container();
mockContainer.register('store:main', Ember.Object.extend({
find: function() { ... }
});
var controller = App.PostController.create({ container: mockContainer });
If you need access to the real store then you can just grab the controller from your App's container.
var controller = App.__container__.lookup('controller:post');
That will instantiate a PostController for you that has all of it's dependencies (such as store) wired together.

Jasmine testing with Templates

I'm trying to use Jasmine (gem with yaml config) to test a Backbone.js application. I'm using the underscore templating like the Todo example.
template: _.template($('#item-template').html())
My problem is that I'm unable to have the templates loaded before my models/views so the template call causes those classes to error out at load.
I've read about the jasmine-jquery plugin to do fixtures but the problem is that my src files (models/views) are being loaded and failing before I ever get the the spec file and am able to setup the fixtures needed.
How do I get the templates loaded early enough they can be used for the reset of my classes?
You can delay the jQuery selector until you need it:
render: function(){
var templateHtml = $(this.template).html();
_.template(templateHtml);
}
Or you can run the selector when the view initializes:
initialize: function(){
this.template = _.template($(this.template).html());
}
Or, if you really want to leave your code as-is and have the selector evaluate when you define the View, you can wrap all of your Backbone code in a function that you call when you want to initialize your entire app code... such as a jQuery $(function(){} function on your real HTML page, or a beforeEach function in your Jasmine tests:
MyApp = (function(){
var myApp = {};
myApp.MyView = Backbone.View.extend({
template: _.template($("#item-template").html())
// ...
});
return myApp;
});
Then in your app, to start this up:
$(function(){
var myApp = MyApp();
new myApp.MyView();
// ...
});
And in your Jasmine test:
describe("how this thing works", function(){
beforeEach(function(){
var myApp = MyApp();
this.view = new myApp.MyView();
// ...
});
});
Once you have one of these solutions in place, you can use something like Jasmine-jQuery to load your fixtures.
FWIW: I tend to use a combination of these techniques, as needed.
The one issue with the first part of the accepted answer is that the template gets compiled every time the view is instantiated. One alternative is to set the template directly on the prototype of the view:
app.TodoView = Backbone.View.extend({
initialize: function(){
if (!app.TodoView.prototype.template) {
app.TodoView.prototype.template = _.template($("#item-template").html());
}
}
});
This way, the template is compiled once, when the first instance of TodoView is instantiated.