Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit b7e754a

Browse files
committedFeb 24, 2016
feat(ngMock): add sharedInjector() to angular.mock.module
Allow to opt-in to using a shared injector within a context. This allows hooks to be used in Jasmine 2.x.x/Mocha
1 parent c900b9c commit b7e754a

File tree

3 files changed

+476
-61
lines changed

3 files changed

+476
-61
lines changed
 

‎docs/content/guide/unit-testing.ngdoc

+37
Original file line numberDiff line numberDiff line change
@@ -438,5 +438,42 @@ In tests, you can trigger a digest by calling a scope's {@link ng.$rootScope.Sco
438438
If you don't have a scope in your test, you can inject the {@link ng.$rootScope $rootScope} and call `$apply` on it.
439439
There is also an example of testing promises in the {@link ng.$q#testing `$q` service documentation}.
440440

441+
## Using `beforeAll()`
442+
443+
Jasmine's `beforeAll()` and mocha's `before()` hooks are often useful for sharing test setup - either to reduce test run-time or simply to make for more focussed test cases.
444+
445+
By default, ngMock will create an injector per test case to ensure your tests do not affect each other. However, if we want to use `beforeAll()`, ngMock will have to create the injector before any test cases are run, and share that injector through all the cases for that `describe`. That is where {@link angular.mock.module.sharedInjector module.sharedInjector()} comes in. When it's called within a `describe` block, a single injector is shared between all hooks and test cases run in that block.
446+
447+
In the example below we are testing a service that takes a long time to generate its answer. To avoid having all of the assertions we want to write in a single test case, {@link angular.mock.module.sharedInjector module.sharedInjector()} and Jasmine's `beforeAll()` are used to run the service only one. The test cases then all make assertions about the properties added to the service instance.
448+
449+
```javascript
450+
describe("Deep Thought", function() {
451+
452+
module.sharedInjector();
453+
454+
beforeAll(module("UltimateQuestion"));
455+
456+
beforeAll(inject(function(DeepThought) {
457+
expect(DeepThought.answer).toBe(undefined);
458+
DeepThought.generateAnswer();
459+
}));
460+
461+
it("has calculated the answer correctly", inject(function(DeepThought) {
462+
// Because of sharedInjector, we have access to the instance of the DeepThought service
463+
// that was provided to the beforeAll() hook. Therefore we can test the generated answer
464+
expect(DeepThought.answer).toBe(42);
465+
}));
466+
467+
it("has calculated the answer within the expected time", inject(function(DeepThought) {
468+
expect(DeepThought.runTimeMillennia).toBeLessThan(8000);
469+
}));
470+
471+
it("has double checked the answer", inject(function(DeepThought) {
472+
expect(DeepThought.absolutelySureItIsTheRightAnswer).toBe(true);
473+
}));
474+
475+
});
476+
```
477+
441478
## Sample project
442479
See the [angular-seed](https://github.com/angular/angular-seed) project for an example.

‎src/ngMock/angular-mocks.js

+179-60
Original file line numberDiff line numberDiff line change
@@ -2561,11 +2561,16 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) {
25612561
}];
25622562

25632563

2564-
if (window.jasmine || window.mocha) {
2564+
!(function(jasmineOrMocha) {
2565+
2566+
if(!jasmineOrMocha) {
2567+
return;
2568+
}
25652569

25662570
var currentSpec = null,
2571+
injectorState = new InjectorState,
25672572
annotatedFunctions = [],
2568-
isSpecRunning = function() {
2573+
wasInjectorCreated = function() {
25692574
return !!currentSpec;
25702575
};
25712576

@@ -2577,60 +2582,6 @@ if (window.jasmine || window.mocha) {
25772582
return angular.mock.$$annotate.apply(this, arguments);
25782583
};
25792584

2580-
2581-
(window.beforeEach || window.setup)(function() {
2582-
originalRootElement = null;
2583-
annotatedFunctions = [];
2584-
currentSpec = this;
2585-
});
2586-
2587-
(window.afterEach || window.teardown)(function() {
2588-
var injector = currentSpec.$injector;
2589-
2590-
annotatedFunctions.forEach(function(fn) {
2591-
delete fn.$inject;
2592-
});
2593-
2594-
angular.forEach(currentSpec.$modules, function(module) {
2595-
if (module && module.$$hashKey) {
2596-
module.$$hashKey = undefined;
2597-
}
2598-
});
2599-
2600-
currentSpec.$injector = null;
2601-
currentSpec.$modules = null;
2602-
currentSpec.$providerInjector = null;
2603-
currentSpec = null;
2604-
2605-
if (injector) {
2606-
// Ensure `$rootElement` is instantiated, before checking `originalRootElement`
2607-
var $rootElement = injector.get('$rootElement');
2608-
var rootNode = $rootElement && $rootElement[0];
2609-
var cleanUpNodes = !originalRootElement ? [] : [originalRootElement[0]];
2610-
if (rootNode && (!originalRootElement || rootNode !== originalRootElement[0])) {
2611-
cleanUpNodes.push(rootNode);
2612-
}
2613-
angular.element.cleanData(cleanUpNodes);
2614-
2615-
// Ensure `$destroy()` is available, before calling it
2616-
// (a mocked `$rootScope` might not implement it (or not even be an object at all))
2617-
var $rootScope = injector.get('$rootScope');
2618-
if ($rootScope && $rootScope.$destroy) $rootScope.$destroy();
2619-
}
2620-
2621-
// clean up jquery's fragment cache
2622-
angular.forEach(angular.element.fragments, function(val, key) {
2623-
delete angular.element.fragments[key];
2624-
});
2625-
2626-
MockXhr.$$lastInstance = null;
2627-
2628-
angular.forEach(angular.callbacks, function(val, key) {
2629-
delete angular.callbacks[key];
2630-
});
2631-
angular.callbacks.counter = 0;
2632-
});
2633-
26342585
/**
26352586
* @ngdoc function
26362587
* @name angular.mock.module
@@ -2653,7 +2604,7 @@ if (window.jasmine || window.mocha) {
26532604
*/
26542605
window.module = angular.mock.module = function() {
26552606
var moduleFns = Array.prototype.slice.call(arguments, 0);
2656-
return isSpecRunning() ? workFn() : workFn;
2607+
return wasInjectorCreated() ? workFn() : workFn;
26572608
/////////////////////
26582609
function workFn() {
26592610
if (currentSpec.$injector) {
@@ -2680,6 +2631,165 @@ if (window.jasmine || window.mocha) {
26802631
}
26812632
};
26822633

2634+
module.$$beforeAllHook = (window.before || window.beforeAll);
2635+
module.$$afterAllHook = (window.after || window.afterAll);
2636+
2637+
// purely for testing ngMock itself
2638+
module.$$currentSpec = function(to) {
2639+
if(arguments.length === 0) return to;
2640+
currentSpec = to;
2641+
};
2642+
2643+
/**
2644+
* @ngdoc function
2645+
* @name angular.mock.module.sharedInjector
2646+
* @description
2647+
*
2648+
* *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha
2649+
*
2650+
* This function ensures a single injector will be used for all tests in a given describe context.
2651+
* This contrasts with the default behaviour where a new injector is created per test case.
2652+
*
2653+
* Use sharedInjector when you want to take advantage of Jasmine's `beforeAll()`, or mocha's
2654+
* `before()` methods. Call `module.sharedInjector()` before you setup any other hooks that
2655+
* will create (i.e call `module()`) or use (i.e call `inject()`) the injector.
2656+
*
2657+
* You cannot call `sharedInjector()` from within a context already using `sharedInjector()`.
2658+
*
2659+
* ## Example
2660+
*
2661+
* Typically beforeAll is used to make many assertions about a single operation. This can
2662+
* cut down test run-time as the test setup doesn't need to be re-run, and enabling focussed
2663+
* tests each with a single assertion.
2664+
*
2665+
* ```js
2666+
* describe("Deep Thought", function() {
2667+
*
2668+
* module.sharedInjector();
2669+
*
2670+
* beforeAll(module("UltimateQuestion"));
2671+
*
2672+
* beforeAll(inject(function(DeepThought) {
2673+
* expect(DeepThought.answer).toBe(undefined);
2674+
* DeepThought.generateAnswer();
2675+
* }));
2676+
*
2677+
* it("has calculated the answer correctly", inject(function(DeepThought) {
2678+
* // Because of sharedInjector, we have access to the instance of the DeepThought service
2679+
* // that was provided to the beforeAll() hook. Therefore we can test the generated answer
2680+
* expect(DeepThought.answer).toBe(42);
2681+
* }));
2682+
*
2683+
* it("has calculated the answer within the expected time", inject(function(DeepThought) {
2684+
* expect(DeepThought.runTimeMillennia).toBeLessThan(8000);
2685+
* }));
2686+
*
2687+
* it("has double checked the answer", inject(function(DeepThought) {
2688+
* expect(DeepThought.absolutelySureItIsTheRightAnswer).toBe(true);
2689+
* }));
2690+
*
2691+
* });
2692+
*
2693+
* ```
2694+
*/
2695+
module.sharedInjector = function() {
2696+
if(!(module.$$beforeAllHook && module.$$afterAllHook)) {
2697+
throw Error("sharedInjector() cannot be used unless your test runner defines beforeAll/afterAll");
2698+
}
2699+
2700+
var initialized = false;
2701+
2702+
module.$$beforeAllHook(function() {
2703+
if(injectorState.shared) {
2704+
injectorState.sharedError = Error("sharedInjector() cannot be called inside a context that has already called sharedInjector()");
2705+
throw injectorState.sharedError;
2706+
}
2707+
initialized = true;
2708+
currentSpec = this;
2709+
injectorState.shared = true;
2710+
});
2711+
2712+
module.$$afterAllHook(function() {
2713+
if(initialized) {
2714+
injectorState = new InjectorState;
2715+
module.$$cleanup();
2716+
} else {
2717+
injectorState.sharedError = null;
2718+
}
2719+
});
2720+
};
2721+
2722+
module.$$beforeEach = function() {
2723+
if(injectorState.shared && currentSpec && currentSpec != this) {
2724+
var state = currentSpec;
2725+
currentSpec = this;
2726+
angular.forEach(["$injector","$modules","$providerInjector", "$injectorStrict"], function(k) {
2727+
currentSpec[k] = state[k];
2728+
state[k] = null;
2729+
});
2730+
} else {
2731+
currentSpec = this;
2732+
originalRootElement = null;
2733+
annotatedFunctions = [];
2734+
}
2735+
};
2736+
2737+
module.$$afterEach = function() {
2738+
if(injectorState.cleanupAfterEach()) {
2739+
module.$$cleanup();
2740+
}
2741+
};
2742+
2743+
module.$$cleanup = function() {
2744+
var injector = currentSpec.$injector;
2745+
2746+
annotatedFunctions.forEach(function(fn) {
2747+
delete fn.$inject;
2748+
});
2749+
2750+
angular.forEach(currentSpec.$modules, function(module) {
2751+
if (module && module.$$hashKey) {
2752+
module.$$hashKey = undefined;
2753+
}
2754+
});
2755+
2756+
currentSpec.$injector = null;
2757+
currentSpec.$modules = null;
2758+
currentSpec.$providerInjector = null;
2759+
currentSpec = null;
2760+
2761+
if (injector) {
2762+
// Ensure `$rootElement` is instantiated, before checking `originalRootElement`
2763+
var $rootElement = injector.get('$rootElement');
2764+
var rootNode = $rootElement && $rootElement[0];
2765+
var cleanUpNodes = !originalRootElement ? [] : [originalRootElement[0]];
2766+
if (rootNode && (!originalRootElement || rootNode !== originalRootElement[0])) {
2767+
cleanUpNodes.push(rootNode);
2768+
}
2769+
angular.element.cleanData(cleanUpNodes);
2770+
2771+
// Ensure `$destroy()` is available, before calling it
2772+
// (a mocked `$rootScope` might not implement it (or not even be an object at all))
2773+
var $rootScope = injector.get('$rootScope');
2774+
if ($rootScope && $rootScope.$destroy) $rootScope.$destroy();
2775+
}
2776+
2777+
// clean up jquery's fragment cache
2778+
angular.forEach(angular.element.fragments, function(val, key) {
2779+
delete angular.element.fragments[key];
2780+
});
2781+
2782+
MockXhr.$$lastInstance = null;
2783+
2784+
angular.forEach(angular.callbacks, function(val, key) {
2785+
delete angular.callbacks[key];
2786+
});
2787+
angular.callbacks.counter = 0;
2788+
};
2789+
2790+
(window.beforeEach || window.setup)(module.$$beforeEach);
2791+
(window.afterEach || window.teardown)(module.$$afterEach);
2792+
26832793
/**
26842794
* @ngdoc function
26852795
* @name angular.mock.inject
@@ -2782,7 +2892,7 @@ if (window.jasmine || window.mocha) {
27822892
window.inject = angular.mock.inject = function() {
27832893
var blockFns = Array.prototype.slice.call(arguments, 0);
27842894
var errorForStack = new Error('Declaration Location');
2785-
return isSpecRunning() ? workFn.call(currentSpec) : workFn;
2895+
return wasInjectorCreated() ? workFn.call(currentSpec) : workFn;
27862896
/////////////////////
27872897
function workFn() {
27882898
var modules = currentSpec.$modules || [];
@@ -2830,7 +2940,7 @@ if (window.jasmine || window.mocha) {
28302940

28312941
angular.mock.inject.strictDi = function(value) {
28322942
value = arguments.length ? !!value : true;
2833-
return isSpecRunning() ? workFn() : workFn;
2943+
return wasInjectorCreated() ? workFn() : workFn;
28342944

28352945
function workFn() {
28362946
if (value !== currentSpec.$injectorStrict) {
@@ -2842,4 +2952,13 @@ if (window.jasmine || window.mocha) {
28422952
}
28432953
}
28442954
};
2845-
}
2955+
2956+
function InjectorState() {
2957+
this.shared = false;
2958+
this.sharedError = null;
2959+
2960+
this.cleanupAfterEach = function() {
2961+
return !this.shared || this.sharedError;
2962+
}
2963+
}
2964+
})(window.jasmine || window.mocha);

‎test/ngMock/angular-mocksSpec.js

+260-1
Original file line numberDiff line numberDiff line change
@@ -2397,7 +2397,6 @@ describe('make sure that we can create an injector outside of tests', function()
23972397
angular.injector([function($injector) {}]);
23982398
});
23992399

2400-
24012400
describe('`afterEach` clean-up', function() {
24022401
describe('`$rootElement`', function() {
24032402
describe('undecorated', function() {
@@ -2561,3 +2560,263 @@ describe('`afterEach` clean-up', function() {
25612560
});
25622561
});
25632562
});
2563+
2564+
describe('sharedInjector', function() {
2565+
// this is of a bit tricky feature to test as we hit angular's own testing
2566+
// mechanisms (e.g around jQuery cache checking), as ngMock augments the very
2567+
// jasmine test runner we're using to test ngMock!
2568+
//
2569+
// with that in mind, we define a stubbed test framework
2570+
// to simulate test cases being run with the ngMock hooks
2571+
2572+
2573+
// we use the 'module' and 'inject' globals from ngMock
2574+
2575+
it("allowes me to mutate a single instace of a module (proving it has been shared)", ngMockTest(function() {
2576+
sdescribe("test state is shared", function() {
2577+
angular.module("sharedInjectorTestModuleA", [])
2578+
.factory("testService", function() {
2579+
return { state: 0 };
2580+
});
2581+
2582+
module.sharedInjector();
2583+
2584+
sbeforeAll(module("sharedInjectorTestModuleA"));
2585+
2586+
sit("access and mutate", inject(function(testService) {
2587+
testService.state += 1;
2588+
}));
2589+
2590+
sit("expect mutation to have persisted", inject(function(testService) {
2591+
expect(testService.state).toEqual(1);
2592+
}));
2593+
});
2594+
}));
2595+
2596+
2597+
it("works with standard beforeEach", ngMockTest(function() {
2598+
sdescribe("test state is not shared", function() {
2599+
angular.module("sharedInjectorTestModuleC", [])
2600+
.factory("testService", function() {
2601+
return { state: 0 };
2602+
});
2603+
2604+
sbeforeEach(module("sharedInjectorTestModuleC"));
2605+
2606+
sit("access and mutate", inject(function(testService) {
2607+
testService.state += 1;
2608+
}));
2609+
2610+
sit("expect mutation not to have persisted", inject(function(testService) {
2611+
expect(testService.state).toEqual(0);
2612+
}));
2613+
});
2614+
}));
2615+
2616+
2617+
it('allows me to stub with shared injector', ngMockTest(function() {
2618+
sdescribe("test state is shared", function() {
2619+
angular.module("sharedInjectorTestModuleD", [])
2620+
.value("testService", 43);
2621+
2622+
module.sharedInjector();
2623+
2624+
sbeforeAll(module("sharedInjectorTestModuleD", function($provide) {
2625+
$provide.value("testService", 42);
2626+
}));
2627+
2628+
sit("expected access stubbed value", inject(function(testService) {
2629+
expect(testService).toEqual(42);
2630+
}));
2631+
})
2632+
}));
2633+
2634+
it("doesn't interfere with other test describes", ngMockTest(function() {
2635+
angular.module("sharedInjectorTestModuleE", [])
2636+
.factory("testService", function() {
2637+
return { state: 0 };
2638+
});
2639+
2640+
sdescribe("with stubbed injector", function() {
2641+
2642+
module.sharedInjector();
2643+
2644+
sbeforeAll(module("sharedInjectorTestModuleE"));
2645+
2646+
sit("access and mutate", inject(function(testService) {
2647+
expect(testService.state).toEqual(0);
2648+
testService.state += 1;
2649+
}));
2650+
2651+
sit("expect mutation to have persisted", inject(function(testService) {
2652+
expect(testService.state).toEqual(1);
2653+
}));
2654+
});
2655+
2656+
sdescribe("without stubbed injector", function() {
2657+
sbeforeEach(module("sharedInjectorTestModuleE"));
2658+
2659+
sit("access and mutate", inject(function(testService) {
2660+
expect(testService.state).toEqual(0);
2661+
testService.state += 1;
2662+
}));
2663+
2664+
sit("expect original, unmutated value", inject(function(testService) {
2665+
expect(testService.state).toEqual(0);
2666+
}));
2667+
});
2668+
}));
2669+
2670+
it("prevents nested use of sharedInjector()", function() {
2671+
var test = ngMockTest(function() {
2672+
sdescribe("outer", function() {
2673+
2674+
module.sharedInjector();
2675+
2676+
sdescribe("inner", function() {
2677+
2678+
module.sharedInjector();
2679+
2680+
sit("should not get here", function() {
2681+
throw Error("should have thrown before here!");
2682+
});
2683+
});
2684+
2685+
});
2686+
2687+
});
2688+
2689+
assertThrowsErrorMatching(test.bind(this), /already called sharedInjector()/);
2690+
});
2691+
2692+
it('warns that shared injector cannot be used unless test frameworks define before/after all hooks', function() {
2693+
assertThrowsErrorMatching(function() {
2694+
module.sharedInjector();
2695+
}, /sharedInjector()/);
2696+
})
2697+
2698+
function assertThrowsErrorMatching(fn, re) {
2699+
try {
2700+
fn();
2701+
} catch(e) {
2702+
if(re.test(e.message)) {
2703+
return;
2704+
}
2705+
throw Error("thrown error '" + e.message + "' did not match:" + re);
2706+
}
2707+
throw Error("should have thrown error");
2708+
}
2709+
2710+
// run a set of test cases in the sdescribe stub test framework
2711+
function ngMockTest(define) {
2712+
return function() {
2713+
var spec = this;
2714+
module.$$currentSpec(null);
2715+
2716+
// configure our stubbed test framework and then hook ngMock into it
2717+
// in much the same way
2718+
module.$$beforeAllHook = sbeforeAll;
2719+
module.$$afterAllHook = safterAll;
2720+
2721+
sdescribe.root = sdescribe("root", function() {});
2722+
2723+
sdescribe.root.beforeEach.push(module.$$beforeEach);
2724+
sdescribe.root.afterEach.push(module.$$afterEach);
2725+
2726+
try {
2727+
define();
2728+
sdescribe.root.run();
2729+
} finally {
2730+
// avoid failing testability for the additional
2731+
// injectors etc created
2732+
angular.element.cache = {};
2733+
2734+
// clear up
2735+
module.$$beforeAllHook = null;
2736+
module.$$afterAllHook = null;
2737+
module.$$currentSpec(spec);
2738+
}
2739+
}
2740+
}
2741+
2742+
// stub test framework that follows the pattern of hooks that
2743+
// jasmine/mocha do
2744+
function sdescribe(name, define) {
2745+
var self = { name: name };
2746+
self.parent = sdescribe.current || sdescribe.root;
2747+
if(self.parent) {
2748+
self.parent.describes.push(self);
2749+
}
2750+
2751+
var previous = sdescribe.current;
2752+
sdescribe.current = self;
2753+
2754+
self.beforeAll = [];
2755+
self.beforeEach = [];
2756+
self.afterAll = [];
2757+
self.afterEach = [];
2758+
self.define = define;
2759+
self.tests = [];
2760+
self.describes = [];
2761+
2762+
self.run = function() {
2763+
var spec = {};
2764+
self.hooks("beforeAll", spec);
2765+
2766+
self.tests.forEach(function(test) {
2767+
if(self.parent) self.parent.hooks("beforeEach", spec);
2768+
self.hooks("beforeEach", spec);
2769+
test.run.call(spec);
2770+
self.hooks("afterEach", spec);
2771+
if(self.parent) self.parent.hooks("afterEach", spec);
2772+
});
2773+
2774+
self.describes.forEach(function(d) {
2775+
d.run();
2776+
});
2777+
2778+
self.hooks("afterAll", spec);
2779+
};
2780+
2781+
self.hooks = function(hook, spec) {
2782+
self[hook].forEach(function(f) {
2783+
f.call(spec);
2784+
})
2785+
}
2786+
2787+
define();
2788+
2789+
sdescribe.current = previous;
2790+
2791+
return self;
2792+
}
2793+
2794+
function sit(name, fn) {
2795+
if(typeof fn !== "function") throw Error("not fn", fn);
2796+
sdescribe.current.tests.push({
2797+
name: name,
2798+
run: fn,
2799+
});
2800+
}
2801+
2802+
function sbeforeAll(fn) {
2803+
if(typeof fn !== "function") throw Error("not fn", fn);
2804+
sdescribe.current.beforeAll.push(fn);
2805+
}
2806+
2807+
function safterAll(fn) {
2808+
if(typeof fn !== "function") throw Error("not fn", fn);
2809+
sdescribe.current.afterAll.push(fn);
2810+
}
2811+
2812+
function sbeforeEach(fn) {
2813+
if(typeof fn !== "function") throw Error("not fn", fn);
2814+
sdescribe.current.beforeEach.push(fn);
2815+
}
2816+
2817+
function safterEach(fn) {
2818+
if(typeof fn !== "function") throw Error("not fn", fn);
2819+
sdescribe.current.afterEach.push(fn);
2820+
}
2821+
2822+
})

0 commit comments

Comments
 (0)
This repository has been archived.