8000 feat(ngMock): add sharedInjector() to angular.mock.module · angular/angular.js@a46ab60 · GitHub
[go: up one dir, main page]

Skip to content
< 8000 script crossorigin="anonymous" type="application/javascript" src="https://github.githubassets.com/assets/vendors-node_modules_github_remote-form_dist_index_js-node_modules_delegated-events_dist_inde-94fd67-99b04cc350b5.js" defer="defer">
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit a46ab60

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 #14093 Closes #10238
1 parent 4e30e4a commit a46ab60

File tree

3 files changed

+484
-61
lines changed

3 files changed

+484
-61
lines changed

docs/content/guide/unit-testing.ngdoc

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,5 +430,50 @@ If your directive uses `templateUrl`, consider using
430430
to pre-compile HTML templates and thus avoid having to load them over HTTP during test execution.
431431
Otherwise you may run into issues if the test directory hierarchy differs from the application's.
432432

433+
## Testing Promises
434+
435+
When testing promises, it's important to know that the resolution of promises is tied to the {@link ng.$rootScope.Scope#$digest digest cycle}.
436+
That means a promise's `then`, `catch` and `finally` callback functions are only called after a digest has run.
437+
In tests, you can trigger a digest by calling a scope's {@link ng.$rootScope.Scope#$apply `$apply` function}.
438+
If you don't have a scope in your test, you can inject the {@link ng.$rootScope $rootScope} and call `$apply` on it.
439+
There is also an example of testing promises in the {@link ng.$q#testing `$q` service documentation}.
440+
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+
433478
## Sample project
434479
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,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)
0