Skip to content

Commit af0574e

Browse files
timrufflespetebacondarwin
authored andcommitted
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 Closes angular#14093 Closes angular#10238
1 parent 4883e95 commit af0574e

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,14 +2582,165 @@ if (window.jasmine || window.mocha) {
25772582
return angular.mock.$$annotate.apply(this, arguments);
25782583
};
25792584

2585+
/**
2586+
* @ngdoc function
2587+
* @name angular.mock.module
2588+
* @description
2589+
*
2590+
* *NOTE*: This function is also published on window for easy access.<br>
2591+
* *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha
2592+
*
2593+
* This function registers a module configuration code. It collects the configuration information
2594+
* which will be used when the injector is created by {@link angular.mock.inject inject}.
2595+
*
2596+
* See {@link angular.mock.inject inject} for usage example
2597+
*
2598+
* @param {...(string|Function|Object)} fns any number of modules which are represented as string
2599+
* aliases or as anonymous module initialization functions. The modules are used to
2600+
* configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. If an
2601+
* object literal is passed each key-value pair will be registered on the module via
2602+
* {@link auto.$provide $provide}.value, the key being the string name (or token) to associate
2603+
* with the value on the injector.
2604+
*/
2605+
var module = window.module = angular.mock.module = function() {
2606+
var moduleFns = Array.prototype.slice.call(arguments, 0);
2607+
return wasInjectorCreated() ? workFn() : workFn;
2608+
/////////////////////
2609+
function workFn() {
2610+
if (currentSpec.$injector) {
2611+
throw new Error('Injector already created, can not register a module!');
2612+
} else {
2613+
var fn, modules = currentSpec.$modules || (currentSpec.$modules = []);
2614+
angular.forEach(moduleFns, function(module) {
2615+
if (angular.isObject(module) && !angular.isArray(module)) {
2616+
fn = ['$provide', function($provide) {
2617+
angular.forEach(module, function(value, key) {
2618+
$provide.value(key, value);
2619+
});
2620+
}];
2621+
} else {
2622+
fn = module;
2623+
}
2624+
if (currentSpec.$providerInjector) {
2625+
currentSpec.$providerInjector.invoke(fn);
2626+
} else {
2627+
modules.push(fn);
2628+
}
2629+
});
2630+
}
2631+
}
2632+
};
25802633

2581-
(window.beforeEach || window.setup)(function() {
2582-
originalRootElement = null;
2583-
annotatedFunctions = [];
2584-
currentSpec = this;
2585-
});
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+
});
25862711

2587-
(window.afterEach || window.teardown)(function() {
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() {
25882744
var injector = currentSpec.$injector;
25892745

25902746
annotatedFunctions.forEach(function(fn) {
@@ -2629,57 +2785,11 @@ if (window.jasmine || window.mocha) {
26292785
delete angular.callbacks[key];
26302786
});
26312787
angular.callbacks.counter = 0;
2632-
});
2633-
2634-
/**
2635-
* @ngdoc function
2636-
* @name angular.mock.module
2637-
* @description
2638-
*
2639-
* *NOTE*: This function is also published on window for easy access.<br>
2640-
* *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha
2641-
*
2642-
* This function registers a module configuration code. It collects the configuration information
2643-
* which will be used when the injector is created by {@link angular.mock.inject inject}.
2644-
*
2645-
* See {@link angular.mock.inject inject} for usage example
2646-
*
2647-
* @param {...(string|Function|Object)} fns any number of modules which are represented as string
2648-
* aliases or as anonymous module initialization functions. The modules are used to
2649-
* configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. If an
2650-
* object literal is passed each key-value pair will be registered on the module via
2651-
* {@link auto.$provide $provide}.value, the key being the string name (or token) to associate
2652-
* with the value on the injector.
2653-
*/
2654-
window.module = angular.mock.module = function() {
2655-
var moduleFns = Array.prototype.slice.call(arguments, 0);
2656-
return isSpecRunning() ? workFn() : workFn;
2657-
/////////////////////
2658-
function workFn() {
2659-
if (currentSpec.$injector) {
2660-
throw new Error('Injector already created, can not register a module!');
2661-
} else {
2662-
var fn, modules = currentSpec.$modules || (currentSpec.$modules = []);
2663-
angular.forEach(moduleFns, function(module) {
2664-
if (angular.isObject(module) && !angular.isArray(module)) {
2665-
fn = ['$provide', function($provide) {
2666-
angular.forEach(module, function(value, key) {
2667-
$provide.value(key, value);
2668-
});
2669-
}];
2670-
} else {
2671-
fn = module;
2672-
}
2673-
if (currentSpec.$providerInjector) {
2674-
currentSpec.$providerInjector.invoke(fn);
2675-
} else {
2676-
modules.push(fn);
2677-
}
2678-
});
2679-
}
2680-
}
26812788
};
26822789

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)