Skip to content
Permalink
Browse files

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
  • Loading branch information...
petebacondarwin authored and kara committed Mar 22, 2019
1 parent 5e53956 commit 3fb78aaaccf32b72a7b9eff55f144fb39e74474f
@@ -115,6 +115,7 @@ module.exports =
'service-worker/index.ts',
'upgrade/index.ts',
'upgrade/static/index.ts',
'upgrade/static/testing/index.ts',
];

readFilesProcessor.fileReaders.push(packageContentFileReader);
@@ -24,7 +24,7 @@ const packageMap = {
'platform-webworker-dynamic': ['platform-webworker-dynamic/index.ts'],
router: ['router/index.ts', 'router/testing/index.ts', 'router/upgrade/index.ts'],
'service-worker': ['service-worker/index.ts'],
upgrade: ['upgrade/index.ts', 'upgrade/static/index.ts']
upgrade: ['upgrade/index.ts', 'upgrade/static/index.ts', 'upgrade/static/testing/index.ts']
};


@@ -39,6 +39,7 @@ import * as routerUpgrade from '@angular/router/upgrade';
import * as serviceWorker from '@angular/service-worker';
import * as upgrade from '@angular/upgrade';
import * as upgradeStatic from '@angular/upgrade/static';
import * as upgradeTesting from '@angular/upgrade/static/testing';

export default {
animations,
@@ -71,5 +72,6 @@ export default {
routerUpgrade,
serviceWorker,
upgrade,
upgradeStatic
upgradeStatic,
upgradeTesting,
};
@@ -0,0 +1,46 @@
/**
* @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
*/

// #docregion angular-setup
import {TestBed} from '@angular/core/testing';
import {createAngularJSTestingModule, createAngularTestingModule} from '@angular/upgrade/static/testing';

import {HeroesService, Ng2AppModule, ng1AppModule} from './module';

const {module, inject} = (window as any).angular.mock;

// #enddocregion angular-setup
describe('HeroesService (from Angular)', () => {

// #docregion angular-setup
beforeEach(() => {
TestBed.configureTestingModule(
{imports: [createAngularTestingModule([ng1AppModule.name]), Ng2AppModule]});
});
// #enddocregion angular-setup

// #docregion angular-spec
it('should have access to the HeroesService', () => {
const heroesService = TestBed.get(HeroesService) as HeroesService;
expect(heroesService).toBeDefined();
});
// #enddocregion angular-spec
});


describe('HeroesService (from AngularJS)', () => {
// #docregion angularjs-setup
beforeEach(module(createAngularJSTestingModule([Ng2AppModule])));
beforeEach(module(ng1AppModule.name));
// #enddocregion angularjs-setup

// #docregion angularjs-spec
it('should have access to the HeroesService',
inject((heroesService: HeroesService) => { expect(heroesService).toBeDefined(); }));
// #enddocregion angularjs-spec
});
@@ -13,13 +13,13 @@ import {UpgradeComponent, UpgradeModule, downgradeComponent, downgradeInjectable

declare var angular: ng.IAngularStatic;

interface Hero {
export interface Hero {
name: string;
description: string;
}

// #docregion ng1-text-formatter-service
class TextFormatter {
export class TextFormatter {
titleCase(value: string) { return value.replace(/((^|\s)[a-z])/g, (_, c) => c.toUpperCase()); }
}

@@ -38,7 +38,7 @@ class TextFormatter {
</div>
<button (click)="addHero.emit()">Add Hero</button>`,
})
class Ng2HeroesComponent {
export class Ng2HeroesComponent {
@Input() heroes !: Hero[];
@Output() addHero = new EventEmitter();
@Output() removeHero = new EventEmitter();
@@ -48,7 +48,7 @@ class Ng2HeroesComponent {
// #docregion ng2-heroes-service
// This Angular service will be "downgraded" to be used in AngularJS
@Injectable()
class HeroesService {
export class HeroesService {
heroes: Hero[] = [
{name: 'superman', description: 'The man of steel'},
{name: 'wonder woman', description: 'Princess of the Amazons'},
@@ -74,7 +74,7 @@ class HeroesService {
// #docregion ng1-hero-wrapper
// This Angular directive will act as an interface to the "upgraded" AngularJS component
@Directive({selector: 'ng1-hero'})
class Ng1HeroComponentWrapper extends UpgradeComponent {
export class Ng1HeroComponentWrapper extends UpgradeComponent {
// The names of the input and output properties here must match the names of the
// `<` and `&` bindings in the AngularJS component that is being wrapped
@Input() hero !: Hero;
@@ -104,7 +104,7 @@ class Ng1HeroComponentWrapper extends UpgradeComponent {
imports: [BrowserModule, UpgradeModule]
})
// #docregion bootstrap-ng1
class Ng2AppModule {
export class Ng2AppModule {
// #enddocregion ng2-module
constructor(private upgrade: UpgradeModule) {}

@@ -122,7 +122,7 @@ class Ng2AppModule {
// #docregion Angular 1 Stuff
// #docregion ng1-module
// This Angular 1 module represents the AngularJS pieces of the application
const ng1AppModule = angular.module('ng1AppModule', []);
export const ng1AppModule: ng.IModule = angular.module('ng1AppModule', []);
// #enddocregion

// #docregion ng1-hero
@@ -17,10 +17,13 @@ def create_upgrade_example_targets(name, srcs, e2e_srcs, entry_module, assets =
type_check = False,
deps = [
"@npm//@types/angular",
"@npm//@types/jasmine",
"//packages/core",
"//packages/platform-browser",
"//packages/platform-browser-dynamic",
"//packages/upgrade/static",
"//packages/core/testing",
"//packages/upgrade/static/testing",
],
tsconfig = "//packages/examples/upgrade:tsconfig-build.json",
)
@@ -23,6 +23,7 @@ ng_package(
srcs = [
"package.json",
"//packages/upgrade/static:package.json",
"//packages/upgrade/static/testing:package.json",
],
entry_point = ":index.ts",
tags = [
@@ -34,5 +35,6 @@ ng_package(
deps = [
":upgrade",
"//packages/upgrade/static",
"//packages/upgrade/static/testing",
],
)
@@ -234,13 +234,15 @@ let angular: {
(e: string | Element | Document | IAugmentedJQuery): IAugmentedJQuery;
cleanData: (nodes: Node[] | NodeList) => void;
},
injector: (modules: Array<string|IInjectable>, strictDi?: boolean) => IInjectorService,
version: {major: number},
resumeBootstrap: () => void,
getTestability: (e: Element) => ITestabilityService
} = {
bootstrap: noNg,
module: noNg,
element: noNgElement,
injector: noNg,
version: undefined as any,
resumeBootstrap: noNg,
getTestability: noNg
@@ -304,6 +306,9 @@ export const module_: typeof angular.module = (prefix, dependencies?) =>
export const element: typeof angular.element = (e => angular.element(e)) as typeof angular.element;
element.cleanData = nodes => angular.element.cleanData(nodes);

export const injector: typeof angular.injector =
(modules: Array<string|IInjectable>, strictDi?: boolean) => angular.injector(modules, strictDi);

export const resumeBootstrap: typeof angular.resumeBootstrap = () => angular.resumeBootstrap();

export const getTestability: typeof angular.getTestability = e => angular.getTestability(e);
@@ -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",
],
)
@@ -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';
@@ -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"
}
@@ -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';
@@ -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;
}

0 comments on commit 3fb78aa

Please sign in to comment.
You can’t perform that action at this time.