feat(upgrade): provide unit test helpers for wiring up injectors (#16848)

Adds two new helper functions that can be used when unit testing Angular services
that depend upon upgraded AngularJS services, or vice versa.
The functions return a module (AngularJS or NgModule) that is configured to wire up
the Angular and AngularJS injectors without the need to actually bootstrap a full
hybrid application.

This makes it simpler and faster to unit test services.

PR Close #16848
This commit is contained in:
Pete Bacon Darwin
2019-03-22 09:42:52 +00:00
committed by Kara Erickson
parent 5e53956c2b
commit 3fb78aaacc
20 changed files with 506 additions and 9 deletions

View File

@ -0,0 +1,19 @@
load("//tools:defaults.bzl", "ng_module")
package(default_visibility = ["//visibility:public"])
exports_files(["package.json"])
ng_module(
name = "testing",
srcs = glob(
[
"*.ts",
"src/*.ts",
],
),
deps = [
"//packages/core/testing",
"//packages/upgrade/src/common",
],
)

View File

@ -0,0 +1,9 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export * from './public_api';

View File

@ -0,0 +1,11 @@
{
"name": "@angular/upgrade/static/testing",
"main": "../../bundles/upgrade-static-testing.umd.js",
"module": "../../fesm5/static/testing.js",
"es2015": "../../fesm2015/static/testing.js",
"esm5": "../../esm5/static/testing/testing.js",
"esm2015": "../../esm2015/static/testing/testing.js",
"fesm5": "../../fesm5/static/testing.js",
"fesm2015": "../../fesm2015/static/testing.js",
"typings": "./testing.d.ts"
}

View File

@ -0,0 +1,10 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export {createAngularTestingModule} from './src/create_angular_testing_module';
export {createAngularJSTestingModule} from './src/create_angularjs_testing_module';

View File

@ -0,0 +1,99 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injector, NgModule, Type} from '@angular/core';
import * as angular from '../../../src/common/src/angular1';
import {$INJECTOR, INJECTOR_KEY, UPGRADE_APP_TYPE_KEY} from '../../../src/common/src/constants';
import {UpgradeAppType} from '../../../src/common/src/util';
export let $injector: angular.IInjectorService|null = null;
let injector: Injector;
export function $injectorFactory() {
return $injector;
}
@NgModule({providers: [{provide: $INJECTOR, useFactory: $injectorFactory}]})
export class AngularTestingModule {
constructor(i: Injector) { injector = i; }
}
/**
* A helper function to use when unit testing Angular services that depend upon upgraded AngularJS
* services.
*
* This function returns an `NgModule` decorated class that is configured to wire up the Angular
* and AngularJS injectors without the need to actually bootstrap a hybrid application.
* This makes it simpler and faster to unit test services.
*
* Use the returned class as an "import" when configuring the `TestBed`.
*
* In the following code snippet, we are configuring the TestBed with two imports.
* The `Ng2AppModule` is the Angular part of our hybrid application and the `ng1AppModule` is the
* AngularJS part.
*
* <code-example path="upgrade/static/ts/full/module.spec.ts" region="angular-setup"></code-example>
*
* Once this is done we can get hold of services via the Angular `Injector` as normal.
* Services that are (or have dependencies on) an upgraded AngularJS service, will be instantiated
* as needed by the AngularJS `$injector`.
*
* In the following code snippet, `HeroesService` is an Angular service that depends upon an
* AngularJS service, `titleCase`.
*
* <code-example path="upgrade/static/ts/full/module.spec.ts" region="angular-spec"></code-example>
*
* <div class="alert is-important">
*
* This helper is for testing services not Components.
* For Component testing you must still bootstrap a hybrid app. See `UpgradeModule` or
* `downgradeModule` for more information.
*
* </div>
*
* <div class="alert is-important">
*
* The resulting configuration does not wire up AngularJS digests to Zone hooks. It is the
* responsibility of the test writer to call `$rootScope.$apply`, as necessary, to trigger
* AngularJS handlers of async events from Angular.
*
* </div>
*
* <div class="alert is-important">
*
* The helper sets up global variables to hold the shared Angular and AngularJS injectors.
*
* * Only call this helper once per spec.
* * Do not use `createAngularTestingModule` in the same spec as `createAngularJSTestingModule`.
*
* </div>
*
* Here is the example application and its unit tests that use `createAngularTestingModule`
* and `createAngularJSTestingModule`.
*
* <code-tabs>
* <code-pane header="module.spec.ts" path="upgrade/static/ts/full/module.spec.ts"></code-pane>
* <code-pane header="module.ts" path="upgrade/static/ts/full/module.ts"></code-pane>
* </code-tabs>
*
*
* @param angularJSModules a collection of the names of AngularJS modules to include in the
* configuration.
* @param [strictDi] whether the AngularJS injector should have `strictDI` enabled.
*
* @publicApi
*/
export function createAngularTestingModule(
angularJSModules: string[], strictDi?: boolean): Type<any> {
angular.module_('$$angularJSTestingModule', angularJSModules)
.constant(UPGRADE_APP_TYPE_KEY, UpgradeAppType.Static)
.factory(INJECTOR_KEY, () => injector);
$injector = angular.injector(['ng', '$$angularJSTestingModule'], strictDi);
return AngularTestingModule;
}

View File

@ -0,0 +1,100 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injector} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import * as ng from '../../../src/common/src/angular1';
import {$INJECTOR, INJECTOR_KEY, UPGRADE_APP_TYPE_KEY} from '../../../src/common/src/constants';
import {UpgradeAppType} from '../../../src/common/src/util';
/**
* A helper function to use when unit testing AngularJS services that depend upon downgraded Angular
* services.
*
* This function returns an AngularJS module that is configured to wire up the AngularJS and Angular
* injectors without the need to actually bootstrap a hybrid application.
* This makes it simpler and faster to unit test services.
*
* Use the returned AngularJS module in a call to
* [`angular.mocks.module`](https://docs.angularjs.org/api/ngMock/function/angular.mock.module) to
* include this module in the unit test injector.
*
* In the following code snippet, we are configuring the `$injector` with two modules:
* The AngularJS `ng1AppModule`, which is the AngularJS part of our hybrid application and the
* `Ng2AppModule`, which is the Angular part.
*
* <code-example path="upgrade/static/ts/full/module.spec.ts"
* region="angularjs-setup"></code-example>
*
* Once this is done we can get hold of services via the AngularJS `$injector` as normal.
* Services that are (or have dependencies on) a downgraded Angular service, will be instantiated as
* needed by the Angular root `Injector`.
*
* In the following code snippet, `heroesService` is a downgraded Angular service that we are
* accessing from AngularJS.
*
* <code-example path="upgrade/static/ts/full/module.spec.ts"
* region="angularjs-spec"></code-example>
*
* <div class="alert is-important">
*
* This helper is for testing services not components.
* For Component testing you must still bootstrap a hybrid app. See `UpgradeModule` or
* `downgradeModule` for more information.
*
* </div>
*
* <div class="alert is-important">
*
* The resulting configuration does not wire up AngularJS digests to Zone hooks. It is the
* responsibility of the test writer to call `$rootScope.$apply`, as necessary, to trigger
* AngularJS handlers of async events from Angular.
*
* </div>
*
* <div class="alert is-important">
*
* The helper sets up global variables to hold the shared Angular and AngularJS injectors.
*
* * Only call this helper once per spec.
* * Do not use `createAngularJSTestingModule` in the same spec as `createAngularTestingModule`.
*
* </div>
*
* Here is the example application and its unit tests that use `createAngularTestingModule`
* and `createAngularJSTestingModule`.
*
* <code-tabs>
* <code-pane header="module.spec.ts" path="upgrade/static/ts/full/module.spec.ts"></code-pane>
* <code-pane header="module.ts" path="upgrade/static/ts/full/module.ts"></code-pane>
* </code-tabs>
*
*
* @param angularModules a collection of Angular modules to include in the configuration.
*
* @publicApi
*/
export function createAngularJSTestingModule(angularModules: any[]): string {
return ng.module_('$$angularJSTestingModule', [])
.constant(UPGRADE_APP_TYPE_KEY, UpgradeAppType.Static)
.factory(
INJECTOR_KEY,
[
$INJECTOR,
($injector: ng.IInjectorService) => {
TestBed.configureTestingModule({
imports: angularModules,
providers: [{provide: $INJECTOR, useValue: $injector}]
});
return TestBed.get(Injector);
}
])
.name;
}

View File

@ -0,0 +1,29 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ts_library", "ts_web_test_suite")
ts_library(
name = "test_lib",
testonly = True,
srcs = glob([
"**/*.ts",
]),
deps = [
"//packages/core",
"//packages/core/testing",
"//packages/upgrade/src/common",
"//packages/upgrade/src/common/test/helpers",
"//packages/upgrade/static",
"//packages/upgrade/static/testing",
],
)
ts_web_test_suite(
name = "test",
static_files = [
"//:angularjs_scripts",
],
deps = [
":test_lib",
],
)

View File

@ -0,0 +1,48 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injector} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {$INJECTOR} from '../../../src/common/src/constants';
import {withEachNg1Version} from '../../../src/common/test/helpers/common_test_helpers';
import {createAngularTestingModule} from '../src/create_angular_testing_module';
import {AppModule, Inventory, defineAppModule, serverRequestInstance} from './mocks';
withEachNg1Version(() => {
describe('Angular entry point', () => {
it('should allow us to get an upgraded AngularJS service from an Angular service', () => {
defineAppModule();
// Configure an NgModule that has the Angular and AngularJS injectors wired up
TestBed.configureTestingModule({imports: [createAngularTestingModule(['app']), AppModule]});
const inventory = TestBed.get(Inventory) as Inventory;
expect(inventory.serverRequest).toBe(serverRequestInstance);
});
it('should create new injectors when we re-use the helper', () => {
defineAppModule();
TestBed.configureTestingModule({imports: [createAngularTestingModule(['app']), AppModule]});
// Check that the injectors are wired up correctly
TestBed.get(Inventory) as Inventory;
// Grab references to the current injectors
const injector = TestBed.get(Injector);
const $injector = TestBed.get($INJECTOR);
TestBed.resetTestingModule();
TestBed.configureTestingModule({imports: [createAngularTestingModule(['app']), AppModule]});
// Check that the injectors are wired up correctly
TestBed.get(Inventory) as Inventory;
// Check that the new injectors are different to the previous ones.
expect(TestBed.get(Injector)).not.toBe(injector);
expect(TestBed.get($INJECTOR)).not.toBe($injector);
});
});
});

View File

@ -0,0 +1,33 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {getAngularJSGlobal} from '../../../src/common/src/angular1';
import {withEachNg1Version} from '../../../src/common/test/helpers/common_test_helpers';
import {createAngularJSTestingModule} from '../src/create_angularjs_testing_module';
import {AppModule, Inventory, defineAppModule} from './mocks';
withEachNg1Version(() => {
describe('AngularJS entry point', () => {
it('should allow us to get a downgraded Angular service from an AngularJS service', () => {
defineAppModule();
// We have to get the `mock` object from the global `angular` variable, rather than trying to
// import it from `@angular/upgrade/src/common/angular1`, because that file doesn't export
// `ngMock` helpers.
const {inject, module} = getAngularJSGlobal().mock;
// Load the AngularJS bits of the application
module('app');
// Configure an AngularJS module that has the AngularJS and Angular injector wired up
module(createAngularJSTestingModule([AppModule]));
let inventory: any = undefined;
inject(function(shoppingCart: any) { inventory = shoppingCart.inventory; });
expect(inventory).toEqual(jasmine.any(Inventory));
});
});
});

View File

@ -0,0 +1,76 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Inject, Injectable, NgModule} from '@angular/core';
import {downgradeInjectable} from '@angular/upgrade/static';
import * as ng from '../../../src/common/src/angular1';
/*
* This mock application code contains the following services and their dependencies:
*
* shoppingCart (AngularJS)
* -> Inventory (Angular - downgraded)
* -> serverRequest (AngularJS - upgraded)
* -> Logger (Angular - downgraded)
*
* This allows us to test two scenarios:
* * AngularJS -> Angular -> AngularJS
* * Angular -> AngularJS -> Angular
*/
/* START: Angular bits */
@Injectable()
export class Logger {
warn() {}
}
@Injectable()
export class Inventory {
constructor(@Inject('serverRequest') public serverRequest: any) {}
}
export function serverRequestFactory(i: ng.IInjectorService) {
return i.get('serverRequest');
}
@NgModule({
providers: [
Logger,
Inventory,
{provide: 'serverRequest', useFactory: serverRequestFactory, deps: ['$injector']},
]
})
export class AppModule {
}
/* END: Angular bits */
/* START: AngularJS bits */
export const serverRequestInstance: {logger?: Logger} = {};
export const shoppingCartInstance: {inventory?: Inventory} = {};
export function defineAppModule() {
ng.module_('app', [])
.factory('logger', downgradeInjectable(Logger))
.factory('inventory', downgradeInjectable(Inventory))
.factory(
'serverRequest',
[
'logger',
function(logger: Logger) {
serverRequestInstance.logger = logger;
return serverRequestInstance;
}
])
.factory('shoppingCart', [
'inventory',
function(inventory: Inventory) {
shoppingCartInstance.inventory = inventory;
return shoppingCartInstance;
}
]);
}
/* END: AngularJS bits */