I'm writing unit tests for what seems like a pretty simple use case, and I'm running into a problem that's not a showstopper, but it's inconvenient enough that it makes me wonder if I've got something wrong in my basic approach.
The page I'm working on is a simple options page with some text boxes and radio buttons, kind of like this:
<input type="text" ng-model="foo">
The controller code looks like this:
$scope.foo = dataStore.getFoo();
$scope.$watch('foo', function(newValue) {
dataStore.setFoo(newValue);
dataStore.commit();
});Everything works fine, but when replace dataStore with a mock object, I can see that setFoo and commit are being called as soon as the scopes $digest method runs. The troubles me for a couple of reasons. First, it violates my intuition that initializing a form should be a read-only operation. Second, it complicates my unit tests; I want to write a Jasmine test that looks like this:
describe('MyCtrl', function() {
var scope;
var dataStore;
beforeEach(function() {
inject(function($rootScope) {
scope = $rootScope.$new();
});
dataStore = jasmine.createSpyObj('dataStore', ['setFoo', 'commit']);
dataStore.getFoo = function() { return 'hello, world'; };
});
it('called commit after foo is modified', function() {
MyCtrl(scope);
scope.$digest(); // Calls dataStore.commit, but shouldn't.
scope.foo = 'new value';
scope.$digest(); // Calls dataStore.commit again.
expect(dataStore.commit).toHaveBeenCalled();
});
});The intent of this test is to verify that the second call to scope.$digest calls dataStore.commit, but it's not actually verifying anything because the first call to scope.$digest calls dataStore.commit, and the test is written so it can't distinguish the two cases. To make the test do what I really want, it looks like I would need to code it more like this:
it('called commit after foo is modified', function() {
MyCtrl(scope);
scope.$digest();
var oldLength = dataStore.commit.calls.length; // Remember how many unwanted calls there were.
scope.foo = 'new value';
scope.$digest();
expect(dataStore.commit.calls.length > oldLength).toBe(true); // Check for the desired call.
});...or like this:
it('called commit after foo is modified', function() {
MyCtrl(scope);
scope.$digest();
dataStore.commit.reset(); // Forget the unwanted call to commit.
scope.foo = 'new value';
scope.$digest();
expect(dataStore.commit).toHaveBeenCalled(); // Check for the desired call.
});I don't think either version is satisfactory because in order to write either one, I have to know about the extraneous call to dataStore.commit and account for it explicitly in the test. As the complexity of the controller increases, this turns into a substantial amount of boilerplate in the test code.
Does anyone have suggestion for how to make this code correct without adding a lot of complexity?