Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit b7e754a

Browse files
committed
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

Lines changed: 37 additions & 0 deletions
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

Lines changed: 179 additions & 60 deletions
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);

0 commit comments

Comments
 (0)