-
Notifications
You must be signed in to change notification settings - Fork 27.4k
Make controllers used with a bindToController directive easier to test #9425
Comments
+1. Testing bindToController should be simple, like the rest of Angular. |
+1 |
3 similar comments
+1 |
+1 |
+1 |
Whether you use the flag or not, it's not going to set up bindings for you, the compiler does that. Why not just compile some DOM, then the work is done for free |
Can you write an example? there are not docs on how to test when use bindToController? |
something like this it('should populate template', inject(function($compile, $rootScope) {
element = $compile('<div my-directive-with-controller cat-bindings="cats"></div>')($rootScope);
// edit: use isolateScope() rather than scope(), since this is isolate-scopes-only for now
newScope = element.isolateScope();
ctrl = newScope.someId;
expect(ctrl.catBindings).toBeUndefined();
$rootScope.$apply('cats = [{name: "Bobby", weight: 6}, {name: "Parker", weight: 7 )]');
// test that the controller's binding has the right stuff
expect(ctrl.bindings).toEqual([
{ name: "Bobby", weight: 6 },
{ name: "Parker", weight: 7 }
]);
table = element.find('table');
rows = element.find('tr');
// test that the template looks right
expect(rows.eq(0).find('td').eq(0).text()).toBe('Bobby');
expect(rows.eq(0).find('td').eq(1).text()).toBe('6.0 kilos');
expect(rows.eq(1).find('td').eq(0).text()).toBe('Parker');
expect(rows.eq(1).find('td').eq(1).text()).toBe('7.0 kilos');
// okay, we're all done!
})); |
So if I'm not wrong the unit tests would be something like this now: it('should have a default label', inject(function($compile, $rootScope) {
element = $compile('<remove-button></remove-button>')($rootScope);
newScope = element.isolateScope();
ctrl = newScope.someId;
newScope.$apply();
expect(ctrl.label).toBe('Delete');
}));
it('should use the specified label', inject(function($compile, $rootScope) {
element = $compile('<remove-button label="foo label"></remove-button>')($rootScope);
newScope = element.isolateScope();
ctrl = newScope.someId;
newScope.$apply();
expect(ctrl.label).toBe('foo label');
})); |
I'm using this approach - seems to work all right. |
Is working well for me. |
+1 for making later public. You wanna test your controller's behavior, not the directive's side-effects. |
We have intentionally pulled almost all of our logic out of link functions and into directive controllers because controllers are much easier to test. You don't have to build a random DOM fragment and you can easily isolate all of the necessary parameters. Until now. So if we use bindToController we have to take a step back and return to building and parsing DOM fragments by hand, making setup and / or every test more complicated. There has to be a better way to do this... 😐 |
+1 for making later public |
So what sort of testing API do you actually want? Like, how would you envision this working? |
Before we build the controller, we currently set stuff on the scope object that we expect from the attribute bindings (isolated scope):
Now our test can verify that the controller set up its instance properties correctly based on the attribute bindings and / or test controller behavior via methods on the controller. |
If we are using
Or, better yet something like:
|
The root of the problem is that the bound attributes need to be set on the controller instance before the controller function is called. |
@petebacondarwin what do you think? mock $controller service could have an extra parameter for setting up bindings, I guess? The limitation is, they wouldn't be real bindings, and wouldn't result in any $watches or anything being set up. Which is great for some kinds of testing, but not so great for others |
I like that idea. I guess that in most tests you would simply simulate watch driven changes from the outside. |
In Angular 2.0, how will components initialize based on property bindings (when necessary)? Will property bindings be available as constructor parameters somehow, or will some component initialization need to occur after the constructor call completes? |
This concept is very important for cases like:
Highly useful to use attribute bindings, property bindings, etc to allow customization of the component by the consumer. |
Sorry, probably a bit off topic here, but very related to the use case that caused this issue. |
…dings Adds a new mock for the $controller service, in order to simplify testing using the bindToController feature. ```js var dictionaryOfControllerBindings = { data: [ { id: 0, phone: '...', name: '...' }, { id: 1, phone: '...', name: '...' }, ] }; // When the MyCtrl constructor is called, `this.data ~= dictionaryOfControllerBindings.data` $controller(MyCtrl, myLocals, dictionaryOfControllerBindings); ``` Closes angular#9425
…dings Adds a new mock for the $controller service, in order to simplify testing using the bindToController feature. ```js var dictionaryOfControllerBindings = { data: [ { id: 0, phone: '...', name: '...' }, { id: 1, phone: '...', name: '...' }, ] }; // When the MyCtrl constructor is called, `this.data ~= dictionaryOfControllerBindings.data` $controller(MyCtrl, myLocals, dictionaryOfControllerBindings); ``` Closes angular#9425
…dings Adds a new mock for the $controller service, in order to simplify testing using the bindToController feature. ```js var dictionaryOfControllerBindings = { data: [ { id: 0, phone: '...', name: '...' }, { id: 1, phone: '...', name: '...' }, ] }; // When the MyCtrl constructor is called, `this.data ~= dictionaryOfControllerBindings.data` $controller(MyCtrl, myLocals, dictionaryOfControllerBindings); ``` Closes #9425 Closes #11239
Fantastic, thanks! |
👍 |
👍 This is what it was meant to be. |
👍 |
…dings Adds a new mock for the $controller service, in order to simplify testing using the bindToController feature. ```js var dictionaryOfControllerBindings = { data: [ { id: 0, phone: '...', name: '...' }, { id: 1, phone: '...', name: '...' }, ] }; // When the MyCtrl constructor is called, `this.data ~= dictionaryOfControllerBindings.data` $controller(MyCtrl, myLocals, dictionaryOfControllerBindings); ``` Closes angular#9425 Closes angular#11239
+1 |
…dings Adds a new mock for the $controller service, in order to simplify testing using the bindToController feature. ```js var dictionaryOfControllerBindings = { data: [ { id: 0, phone: '...', name: '...' }, { id: 1, phone: '...', name: '...' }, ] }; // When the MyCtrl constructor is called, `this.data ~= dictionaryOfControllerBindings.data` $controller(MyCtrl, myLocals, dictionaryOfControllerBindings); ``` Closes angular#9425 Closes angular#11239
What is someId? in the snippet.
|
@lokeshjainRL I copied it from the other comment. Anyway this issue was closed later with a much easier approach: describe('buttons.RemoveButtonCtrl', function () {
var ctrl, $scope;
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('xxCtrl', {
$scope: scope,
}, {
label: 'foo'
});
}));
it('should have a label', function () {
expect(ctrl.label).toBe('foo');
});
}); (from http://stackoverflow.com/questions/25837774/bindtocontroller-in-unit-tests) Pass the initial scope you want to test defaults against as the third argument to |
@ernestoalejo Thanks for pointing to the resources. But this approach is not when you have complex In directive definition return{
bindToController:{
},
scope:{},
controller: 'directiveController',
controllerAs; 'someId'
} That |
There doesn't seem to be a clear mechanism of unit-testing a controller that's used in a directive with
bindToController: true
. These controllers assume certain properties being set before the execution of the constructor.I could build a mechanism myself using a temporary constructor etc. but it would be nice to have something in Angular itself.
I discovered the private boolean arg
later
in the$controller
service that serves the purpose:Would you consider making that flag public and thus officially supported?
Or maybe add a helper to angular mock?
See also this Stack Overflow question.
The text was updated successfully, but these errors were encountered: