aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMisko Hevery2011-11-04 12:33:01 -0700
committerMisko Hevery2011-11-14 20:31:14 -0800
commita87f2fb9e4d65ac5d260e914b5e31aa0e0f47b2c (patch)
tree93e69475affd24ef5b16c68e47d1476bc37787a7
parentc27aba4354c69c4a67fab587a59a8079cc9edc91 (diff)
downloadangular.js-a87f2fb9e4d65ac5d260e914b5e31aa0e0f47b2c.tar.bz2
refactor(mock): moved mocks into its own module
-rw-r--r--src/Angular.js14
-rw-r--r--src/angular-mocks.js106
-rw-r--r--test/BinderSpec.js5
-rw-r--r--test/ResourceSpec.js4
-rw-r--r--test/angular-mocksSpec.js54
-rw-r--r--test/matchers.js17
-rw-r--r--test/scenario/RunnerSpec.js4
-rw-r--r--test/scenario/matchersSpec.js6
-rw-r--r--test/service/filter/orderBySpec.js15
-rw-r--r--test/service/logSpec.js2
-rw-r--r--test/testabilityPatch.js131
11 files changed, 219 insertions, 139 deletions
diff --git a/src/Angular.js b/src/Angular.js
index fcb13881..af441e1c 100644
--- a/src/Angular.js
+++ b/src/Angular.js
@@ -426,6 +426,17 @@ function trim(value) {
return isString(value) ? value.replace(/^\s*/, '').replace(/\s*$/, '') : value;
}
+/**
+ * @ngdoc function
+ * @name angular.isElement
+ * @function
+ *
+ * @description
+ * Determines if a reference is a DOM element (or wrapped jQuery element).
+ *
+ * @param {*} value Reference to check.
+ * @returns {boolean} True if `value` is a DOM element (or wrapped jQuery element).
+ */
function isElement(node) {
return node &&
(node.nodeName // we are a direct element
@@ -1012,7 +1023,7 @@ function assertArg(arg, name, reason) {
function assertArgFn(arg, name) {
assertArg(isFunction(arg), name, 'not a function, got ' +
- (typeof arg == 'object' ? arg.constructor.name : typeof arg));
+ (typeof arg == 'object' ? arg.constructor.name || 'Object' : typeof arg));
return arg;
}
@@ -1034,6 +1045,7 @@ function publishExternalAPI(angular){
'isFunction': isFunction,
'isObject': isObject,
'isNumber': isNumber,
+ 'isElement': isElement,
'isArray': isArray,
'version': version,
'isDate': isDate,
diff --git a/src/angular-mocks.js b/src/angular-mocks.js
index 497fdc58..970a1b6d 100644
--- a/src/angular-mocks.js
+++ b/src/angular-mocks.js
@@ -363,16 +363,21 @@ angular.mock.$ExceptionHandlerProvider = function(){
var handler;
this.mode = function(mode){
- handler = {
- rethrow: function(e) {
- throw e;
- },
- log: angular.extend(function log(e) {
- log.errors.push(e);
- }, {errors:[]})
- }[mode];
- if (!handler) {
- throw Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!");
+ switch(mode) {
+ case 'rethrow':
+ handler = function(e) {
+ throw e;
+ }
+ break;
+ case 'log':
+ var errors = [];
+ handler = function(e) {
+ errors.push(e);
+ }
+ handler.errors = errors;
+ break;
+ default:
+ throw Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!");
}
};
@@ -396,6 +401,12 @@ angular.mock.$ExceptionHandlerProvider = function(){
* See {@link angular.mock} for more info on angular mocks.
*/
angular.mock.$LogProvider = function(){
+
+ function concat(array1, array2, index) {
+ return array1.concat(Array.prototype.slice.call(array2, index));
+ }
+
+
this.$get = function () {
var $log = {
log: function() { $log.log.logs.push(concat([], arguments, 0)); },
@@ -416,7 +427,7 @@ angular.mock.$LogProvider = function(){
angular.forEach(['error', 'warn', 'info', 'log'], function(logLevel) {
angular.forEach($log[logLevel].logs, function(log) {
angular.forEach(log, function (logItem) {
- errors.push('MOCK $log (' + logLevel + '): ' + (logItem.stack || logItem));
+ errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' + (logItem.stack || ''));
});
});
});
@@ -569,3 +580,76 @@ angular.mock.TzDate = function (offset, timestamp) {
//make "tzDateInstance instanceof Date" return true
angular.mock.TzDate.prototype = Date.prototype;
+
+
+/**
+ * Method for serializing common objects into strings, useful for debugging.
+ * @param {*} object - any object to turn into string.
+ * @return a serialized string of the argument
+ */
+angular.mock.dump = function(object){
+ var out;
+ if (angular.isElement(object)) {
+ object = angular.element(object);
+ out = angular.element('<div></div>')
+ angular.forEach(object, function(element){
+ out.append(angular.element(element).clone());
+ });
+ out = out.html();
+ } else if (angular.isObject(object)) {
+ if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) {
+ out = serializeScope(object);
+ } else {
+ out = angular.toJson(object, true);
+ }
+ } else {
+ out = String(object);
+ }
+ return out;
+
+ function serializeScope(scope, offset) {
+ offset = offset || ' ';
+ var log = [offset + 'Scope(' + scope.$id + '): {'];
+ for ( var key in scope ) {
+ if (scope.hasOwnProperty(key) && !key.match(/^(\$|this)/)) {
+ log.push(' ' + key + ': ' + angular.toJson(scope[key]));
+ }
+ }
+ var child = scope.$$childHead;
+ while(child) {
+ log.push(serializeScope(child, offset + ' '));
+ child = child.$$nextSibling;
+ }
+ log.push('}');
+ return log.join('\n' + offset);
+ }
+};
+
+window.jstestdriver && (function(window){
+ /**
+ * Global method to output any number of objects into JSTD console. Useful for debugging.
+ */
+ window.dump = function() {
+ var args = [];
+ angular.forEach(arguments, function(arg){
+ args.push(angular.mock.dump(arg));
+ });
+ jstestdriver.console.log.apply(jstestdriver.console, args);
+ };
+})(window);
+
+
+window.jasmine && (function(window){
+ window.inject = function (){
+ var blockFns = Array.prototype.slice.call(arguments, 0);
+ return function(){
+ var injector = this.$injector;
+ if (!injector) {
+ injector = this.$injector = angular.injector('NG', 'NG_MOCK');
+ }
+ for(var i = 0, ii = blockFns.length; i < ii; i++) {
+ injector.invoke(this, blockFns[i]);
+ }
+ };
+ }
+})(window);
diff --git a/test/BinderSpec.js b/test/BinderSpec.js
index 01e61b57..691e2db8 100644
--- a/test/BinderSpec.js
+++ b/test/BinderSpec.js
@@ -1,6 +1,11 @@
'use strict';
describe('Binder', function() {
+
+ function childNode(element, index) {
+ return jqLite(element[0].childNodes[index]);
+ }
+
beforeEach(function() {
this.compileToHtml = function (content) {
var html;
diff --git a/test/ResourceSpec.js b/test/ResourceSpec.js
index 2091a936..57aaffe0 100644
--- a/test/ResourceSpec.js
+++ b/test/ResourceSpec.js
@@ -3,6 +3,10 @@
describe("resource", function() {
var resource, CreditCard, callback;
+ function nakedExpect(obj) {
+ return expect(angular.fromJson(angular.toJson(obj)));
+ }
+
beforeEach(inject(
function($provide) {
$provide.value('$xhr.error', jasmine.createSpy('xhr.error'));
diff --git a/test/angular-mocksSpec.js b/test/angular-mocksSpec.js
index b4ceb275..1668d500 100644
--- a/test/angular-mocksSpec.js
+++ b/test/angular-mocksSpec.js
@@ -283,4 +283,58 @@ describe('mocks', function() {
}).toThrow("Unknown mode 'XXX', only 'log'/'rethrow' modes are allowed!");
}));
});
+
+
+ describe('angular.mock.debug', function(){
+ var d = angular.mock.dump;
+
+
+ it('should serialize primitive types', function(){
+ expect(d(undefined)).toEqual('undefined');
+ expect(d(1)).toEqual('1');
+ expect(d(null)).toEqual('null');
+ expect(d('abc')).toEqual('abc');
+ });
+
+
+ it('should serialize element', function(){
+ var e = angular.element('<div>abc</div><span>xyz</span>');
+ expect(d(e).toLowerCase()).toEqual('<div>abc</div><span>xyz</span>');
+ expect(d(e[0]).toLowerCase()).toEqual('<div>abc</div>');
+ });
+
+ it('should serialize scope', inject(function($rootScope){
+ $rootScope.obj = {abc:'123'};
+ expect(d($rootScope)).toMatch(/Scope\(.*\): \{/);
+ expect(d($rootScope)).toMatch(/{"abc":"123"}/);
+ }));
+
+
+ it('should be published on window', function(){
+ expect(window.dump instanceof Function).toBe(true);
+ });
+ });
+
+ describe('jasmine inject', function(){
+ it('should call invoke', function(){
+ var count = 0;
+ function fn1(){
+ expect(this).toBe(self);
+ count++;
+ }
+ function fn2(){
+ expect(this).toBe(self);
+ count++;
+ }
+ var fn = inject(fn1, fn2);
+ var self = {
+ $injector: {
+ invoke: function(self, fn) { fn.call(self); }
+ }
+ };
+
+ fn.call(self);
+ expect(count).toBe(2);
+ });
+ });
});
diff --git a/test/matchers.js b/test/matchers.js
index 9923bd7e..fbe86a5a 100644
--- a/test/matchers.js
+++ b/test/matchers.js
@@ -23,6 +23,13 @@ beforeEach(function() {
};
}
+ function indexOf(array, obj) {
+ for ( var i = 0; i < array.length; i++) {
+ if (obj === array[i]) return i;
+ }
+ return -1;
+ }
+
this.addMatchers({
toBeInvalid: cssMatcher('ng-invalid', 'ng-valid'),
toBeValid: cssMatcher('ng-valid', 'ng-invalid'),
@@ -84,6 +91,16 @@ beforeEach(function() {
toBeOneOf: function() {
return indexOf(arguments, this.actual) !== -1;
+ },
+
+ toHaveClass: function(clazz) {
+ this.message = function() {
+ return "Expected '" + angular.mock.dump(this.actual) + "' to have class '" + clazz + "'.";
+ };
+ return this.actual.hasClass ?
+ this.actual.hasClass(clazz) :
+ angular.element(this.actual).hasClass(clazz);
}
+
});
});
diff --git a/test/scenario/RunnerSpec.js b/test/scenario/RunnerSpec.js
index 62d84fca..15bcc4b0 100644
--- a/test/scenario/RunnerSpec.js
+++ b/test/scenario/RunnerSpec.js
@@ -46,8 +46,8 @@ describe('angular.scenario.Runner', function() {
runner.createSpecRunner_ = function(scope) {
return scope.$new(MockSpecRunner);
};
- runner.on('SpecError', rethrow);
- runner.on('StepError', rethrow);
+ runner.on('SpecError', angular.mock.rethrow);
+ runner.on('StepError', angular.mock.rethrow);
});
afterEach(function() {
diff --git a/test/scenario/matchersSpec.js b/test/scenario/matchersSpec.js
index 7ab41cf2..7a5217d7 100644
--- a/test/scenario/matchersSpec.js
+++ b/test/scenario/matchersSpec.js
@@ -42,4 +42,10 @@ describe('angular.scenario.matchers', function () {
expectMatcher(3, function() { matchers.toBeLessThan(10); });
expectMatcher(3, function() { matchers.toBeGreaterThan(-5); });
});
+
+ it('should have toHaveClass matcher', function(){
+ var e = angular.element('<div class="abc">');
+ expect(e).not.toHaveClass('none');
+ expect(e).toHaveClass('abc');
+ });
});
diff --git a/test/service/filter/orderBySpec.js b/test/service/filter/orderBySpec.js
index f59fad55..5c117891 100644
--- a/test/service/filter/orderBySpec.js
+++ b/test/service/filter/orderBySpec.js
@@ -12,18 +12,15 @@ describe('Filter: orderBy', function() {
});
it('shouldSortArrayInReverse', function() {
- assertJsonEquals([{a:15},{a:2}], orderBy([{a:15},{a:2}], 'a', true));
- assertJsonEquals([{a:15},{a:2}], orderBy([{a:15},{a:2}], 'a', "T"));
- assertJsonEquals([{a:15},{a:2}], orderBy([{a:15},{a:2}], 'a', "reverse"));
+ expect(orderBy([{a:15}, {a:2}], 'a', true)).toEqualData([{a:15}, {a:2}]);
+ expect(orderBy([{a:15}, {a:2}], 'a', "T")).toEqualData([{a:15}, {a:2}]);
+ expect(orderBy([{a:15}, {a:2}], 'a', "reverse")).toEqualData([{a:15}, {a:2}]);
});
it('should sort array by predicate', function() {
- assertJsonEquals([{a:2, b:1},{a:15, b:1}],
- orderBy([{a:15, b:1},{a:2, b:1}], ['a', 'b']));
- assertJsonEquals([{a:2, b:1},{a:15, b:1}],
- orderBy([{a:15, b:1},{a:2, b:1}], ['b', 'a']));
- assertJsonEquals([{a:15, b:1},{a:2, b:1}],
- orderBy([{a:15, b:1},{a:2, b:1}], ['+b', '-a']));
+ expect(orderBy([{a:15, b:1}, {a:2, b:1}], ['a', 'b'])).toEqualData([{a:2, b:1}, {a:15, b:1}]);
+ expect(orderBy([{a:15, b:1}, {a:2, b:1}], ['b', 'a'])).toEqualData([{a:2, b:1}, {a:15, b:1}]);
+ expect(orderBy([{a:15, b:1}, {a:2, b:1}], ['+b', '-a'])).toEqualData([{a:15, b:1}, {a:2, b:1}]);
});
it('should use function', function() {
diff --git a/test/service/logSpec.js b/test/service/logSpec.js
index 93e705a9..ee250b66 100644
--- a/test/service/logSpec.js
+++ b/test/service/logSpec.js
@@ -13,7 +13,7 @@ describe('$log', function() {
$window = {};
logger = '';
$provide.service('$log', $LogProvider);
- $provide.value('$exceptionHandler', rethrow);
+ $provide.value('$exceptionHandler', angular.mock.rethrow);
$provide.value('$window', $window);
}));
diff --git a/test/testabilityPatch.js b/test/testabilityPatch.js
index 96deac5a..b2bcb519 100644
--- a/test/testabilityPatch.js
+++ b/test/testabilityPatch.js
@@ -9,46 +9,10 @@
_jQuery.event.special.change = undefined;
-if (window.jstestdriver) {
- window.jstd = jstestdriver;
- window.dump = function dump() {
- var args = [];
- forEach(arguments, function(arg){
- if (isElement(arg)) {
- arg = sortedHtml(arg);
- } else if (isObject(arg)) {
- if (isFunction(arg.$eval) && isFunction(arg.$apply)) {
- arg = dumpScope(arg);
- } else {
- arg = toJson(arg, true);
- }
- }
- args.push(arg);
- });
- jstd.console.log.apply(jstd.console, args);
- };
-}
-
-function dumpScope(scope, offset) {
- offset = offset || ' ';
- var log = [offset + 'Scope(' + scope.$id + '): {'];
- for ( var key in scope ) {
- if (scope.hasOwnProperty(key) && !key.match(/^(\$|this)/)) {
- log.push(' ' + key + ': ' + toJson(scope[key]));
- }
- }
- var child = scope.$$childHead;
- while(child) {
- log.push(dumpScope(child, offset + ' '));
- child = child.$$nextSibling;
- }
- log.push('}');
- return log.join('\n' + offset);
-}
-
-publishExternalAPI(angular)
+publishExternalAPI(angular);
+bindJQuery();
beforeEach(function() {
- publishExternalAPI(angular)
+ publishExternalAPI(angular);
// workaround for IE bug https://plus.google.com/104744871076396904202/posts/Kqjuj6RSbbT
// IE overwrite window.jQuery with undefined because of empty jQuery var statement, so we have to
@@ -63,43 +27,20 @@ beforeEach(function() {
// reset to jQuery or default to us.
bindJQuery();
jqLite(document.body).html('');
-
- this.addMatchers({
- toHaveClass: function(clazz) {
- this.message = function() {
- return "Expected '" + sortedHtml(this.actual) + "' to have class '" + clazz + "'.";
- };
- return this.actual.hasClass ?
- this.actual.hasClass(clazz) :
- jqLite(this.actual).hasClass(clazz);
- }
- });
-
});
-function inject(){
- var blockFns = sliceArgs(arguments);
- return function(){
- var spec = this;
- spec.$injector = spec.$injector || angular.injector('NG', 'NG_MOCK');
- angular.forEach(blockFns, function(fn){
- spec.$injector.invoke(spec, fn);
- });
- };
-}
-
-
-afterEach(inject(function($rootScope, $log) {
- // release the injector
- dealoc($rootScope);
-
- // check $log mock
- $log.assertEmpty && $log.assertEmpty();
+afterEach(function() {
+ if (this.$injector) {
+ var $rootScope = this.$injector('$rootScope');
+ var $log = this.$injector('$log');
+ // release the injector
+ dealoc($rootScope);
- clearJqCache();
-}));
+ // check $log mock
+ $log.assertEmpty && $log.assertEmpty();
+ }
-function clearJqCache() {
+ // complain about uncleared jqCache references
var count = 0;
forEachSorted(jqCache, function(value, key){
count ++;
@@ -115,15 +56,8 @@ function clearJqCache() {
if (count) {
fail('Found jqCache references that were not deallocated!');
}
-}
-
-function nakedExpect(obj) {
- return expect(angular.fromJson(angular.toJson(obj)));
-}
+});
-function childNode(element, index) {
- return jqLite(element[0].childNodes[index]);
-}
function dealoc(obj) {
if (obj) {
@@ -240,43 +174,10 @@ function isCssVisible(node) {
}
function assertHidden(node) {
- assertFalse("Node should be hidden but vas visible: " + sortedHtml(node), isCssVisible(node));
+ assertFalse("Node should be hidden but vas visible: " + angular.mock.dump(node), isCssVisible(node));
}
function assertVisible(node) {
- assertTrue("Node should be visible but vas hidden: " + sortedHtml(node), isCssVisible(node));
-}
-
-function assertJsonEquals(expected, actual) {
- assertEquals(toJson(expected), toJson(actual));
-}
-
-function assertUndefined(value) {
- assertEquals('undefined', typeof value);
+ assertTrue("Node should be visible but vas hidden: " + angular.mock.dump(node), isCssVisible(node));
}
-function assertDefined(value) {
- assertTrue(toJson(value), !!value);
-}
-
-function assertThrows(error, fn){
- var exception = null;
- try {
- fn();
- } catch(e) {
- exception = e;
- }
- if (!exception) {
- fail("Expecting exception, none thrown");
- }
- assertEquals(error, exception);
-}
-
-window.log = noop;
-window.error = noop;
-
-function rethrow(e) {
- if(e) {
- throw e;
- }
-}
pan>MockXHR(){}; var myClass = new MyClass(); myClass.doWork(); // assert that MockXHR got called with the right arguments XHR = oldXHR; // if you forget this bad things will happen </pre> ### Global look-up: Another way to approach the problem is look for the service in a well known location. <pre> function MyClass(){ this.doWork = function(){ global.xhr({ method:'...', url:'...', complete:function(response){ ... } }) } } </pre> While no new instance of dependency is being created, it is fundamentally the same as `new`, in that there is no good way to intercept the call to `global.xhr` for testing purposes, other then through monkey patching. The basic issue for testing is that global variable needs to be mutated in order to replace it with call to a mock method. For further explanation why this is bad see: {@link http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/ Brittle Global State & Singletons} The class above is hard to test since we have to change global state: <pre> var oldXHR = glabal.xhr; glabal.xhr = function mockXHR(){}; var myClass = new MyClass(); myClass.doWork(); // assert that mockXHR got called with the right arguments global.xhr = oldXHR; // if you forget this bad things will happen </pre> ### Service Registry: It may seem as that this can be solved by having a registry for all of the services, and then having the tests replace the services as needed. <pre> function MyClass() { var serviceRegistry = ????; this.doWork = function(){ var xhr = serviceRegistry.get('xhr'); xhr({ method:'...', url:'...', complete:function(response){ ... } }) } </pre> However, where dose the serviceRegistry come from? if it is: * `new`-ed up, the the test has no chance to reset the services for testing * global look-up, then the service returned is global as well (but resetting is easier, since there is only one global variable to be reset). The class above is hard to test since we have to change global state: <pre> var oldServiceLocator = glabal.serviceLocator; glabal.serviceLocator.set('xhr', function mockXHR(){}); var myClass = new MyClass(); myClass.doWork(); // assert that mockXHR got called with the right arguments glabal.serviceLocator = oldServiceLocator; // if you forget this bad things will happen </pre> ### Passing in Dependencies: Lastly the dependency can be passed in. <pre> function MyClass(xhr) { this.doWork = function(){ xhr({ method:'...', url:'...', complete:function(response){ ... } }) } </pre> This is the proferred way since the code makes no assumptions as to where the `xhr` comes from, rather that who-ever crated the class was responsible for passing it in. Since the creator of the class should be different code the the user of the class, it separates the responsibility of creation from the logic, and that is what dependency-injection is in a nutshell. The class above is very testable, since in the test we can write: <pre> function xhrMock(args) {...} var myClass = new MyClass(xhrMock); myClass.doWork(); // assert that xhrMock got called with the right arguments </pre> Notice that no global variables were harmed in the writing of this test. Angular comes with {@link dev_guide.di dependency-injection} built in which makes the right thin the easy thing to do, but you still need to do it if you wish to take advantage of the testability story. ## Controllers What makes each application unique is its logic, which is what we would like to test. If the logic for your application is mixed in with DOM manipulation, it will be hard to test as in the example below: <pre> function PasswordController(){ // get references to DOM elements var msg = $('.ex1 span'); var input = $('.ex1 input'); var strength; this.grade = function(){ msg.removeClass(strength); var pwd = input.val(); password.text(pwd); if (pwd.length > 8) { strength = 'strong'; } else if (pwd.length > 3) { strength = 'medium'; } else { strength = 'weak'; } msg .addClass(strength) .text(strength); } } </pre> The code above is problematic from testability, since it requires your test to have the right kind of DOM present when the code executes. The test would look like this: <pre> var input = $('<input type="text"/>'); var span = $('<span>'); $('body').html('<div class="ex1">') .find('div') .append(input) .append(span); var pc = new PasswordController(); input.val('abc'); pc.grade(); expect(span.text()).toEqual('weak'); $('body').html(''); </pre> In angular the controllers are strictly separated from the DOM manipulation logic which results in a much easier testability story as can be seen in this example: <pre> function PasswordCntrl(){ this.password = ''; this.grade = function(){ var size = this.password.length; if (size > 8) { this.strength = 'strong'; } else if (size > 3) { this.strength = 'medium'; } else { this.strength = 'weak'; } }; } </pre> and the tests is straight forward <pre> var pc = new PasswordController(); pc.password('abc'); pc.grade(); expect(span.strength).toEqual('weak'); </pre> Notice that the test is not only much shorter but it is easier to follow what is going on. We say that such a test tells a story, rather then asserting random bits which don't seem to be related. ## Filters {@link api/angular.filter Filters} are functions which transform the data into user readable format. They are important because they remove the formatting responsibility from the application logic, further simplifying the application logic. <pre> angular.filter('length', function(text){ return (''+(text||'')).length; }); var length = angular.filter('length'); expect(length(null)).toEqual(0); expect(length('abc')).toEqual(3); </pre> ## Directives Directives in angular are responsible for updating the DOM when the state of the model changes. ## Mocks oue ## Global State Isolation oue # Preferred way of Testing uo ## JavaScriptTestDriver ou ## Jasmine ou ## Sample project uoe