function
A helper function to use when unit testing Angular services that depend upon upgraded AngularJS services.
createAngularTestingModule(angularJSModules: string[], strictDi?: boolean): Type<any>
angularJSModules | string[] | a collection of the names of AngularJS modules to include in the configuration. |
strictDi | boolean | whether the AngularJS injector should have Optional. Default is |
Type<any>
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.
import {TestBed} from '@angular/core/testing'; import {createAngularJSTestingModule, createAngularTestingModule} from '@angular/upgrade/static/testing'; import {HeroesService, ng1AppModule, Ng2AppModule} from './module'; const {module, inject} = (window as any).angular.mock; /* . . . */ beforeEach(() => { TestBed.configureTestingModule( {imports: [createAngularTestingModule([ng1AppModule.name]), Ng2AppModule]}); });
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
.
it('should have access to the HeroesService', () => { const heroesService = TestBed.inject(HeroesService); expect(heroesService).toBeDefined(); });
This helper is for testing services not Components. For Component testing you must still bootstrap a hybrid app. See
UpgradeModule
ordowngradeModule
for more information.
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.
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 ascreateAngularJSTestingModule
.
Here is the example application and its unit tests that use createAngularTestingModule
and createAngularJSTestingModule
.
/** * @license * Copyright Google LLC 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 {TestBed} from '@angular/core/testing'; import {createAngularJSTestingModule, createAngularTestingModule} from '@angular/upgrade/static/testing'; import {HeroesService, ng1AppModule, Ng2AppModule} from './module'; const {module, inject} = (window as any).angular.mock; describe('HeroesService (from Angular)', () => { beforeEach(() => { TestBed.configureTestingModule( {imports: [createAngularTestingModule([ng1AppModule.name]), Ng2AppModule]}); }); it('should have access to the HeroesService', () => { const heroesService = TestBed.inject(HeroesService); expect(heroesService).toBeDefined(); }); }); describe('HeroesService (from AngularJS)', () => { beforeEach(module(createAngularJSTestingModule([Ng2AppModule]))); beforeEach(module(ng1AppModule.name)); it('should have access to the HeroesService', inject((heroesService: HeroesService) => { expect(heroesService).toBeDefined(); })); });
/** * @license * Copyright Google LLC 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 {Component, Directive, ElementRef, EventEmitter, Injectable, Injector, Input, NgModule, Output} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {downgradeComponent, downgradeInjectable, UpgradeComponent, UpgradeModule} from '@angular/upgrade/static'; declare var angular: ng.IAngularStatic; export interface Hero { name: string; description: string; } export class TextFormatter { titleCase(value: string) { return value.replace(/((^|\s)[a-z])/g, (_, c) => c.toUpperCase()); } } // This Angular component will be "downgraded" to be used in AngularJS @Component({ selector: 'ng2-heroes', // This template uses the upgraded `ng1-hero` component // Note that because its element is compiled by Angular we must use camelCased attribute names template: `<header><ng-content selector="h1"></ng-content></header> <ng-content selector=".extra"></ng-content> <div *ngFor="let hero of heroes"> <ng1-hero [hero]="hero" (onRemove)="removeHero.emit(hero)"><strong>Super Hero</strong></ng1-hero> </div> <button (click)="addHero.emit()">Add Hero</button>`, }) export class Ng2HeroesComponent { @Input() heroes!: Hero[]; @Output() addHero = new EventEmitter(); @Output() removeHero = new EventEmitter(); } // This Angular service will be "downgraded" to be used in AngularJS @Injectable() export class HeroesService { heroes: Hero[] = [ {name: 'superman', description: 'The man of steel'}, {name: 'wonder woman', description: 'Princess of the Amazons'}, {name: 'thor', description: 'The hammer-wielding god'} ]; constructor(textFormatter: TextFormatter) { // Change all the hero names to title case, using the "upgraded" AngularJS service this.heroes.forEach((hero: Hero) => hero.name = textFormatter.titleCase(hero.name)); } addHero() { this.heroes = this.heroes.concat([{name: 'Kamala Khan', description: 'Epic shape-shifting healer'}]); } removeHero(hero: Hero) { this.heroes = this.heroes.filter((item: Hero) => item !== hero); } } // This Angular directive will act as an interface to the "upgraded" AngularJS component @Directive({selector: 'ng1-hero'}) 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; @Output() onRemove!: EventEmitter<void>; constructor(elementRef: ElementRef, injector: Injector) { // We must pass the name of the directive as used by AngularJS to the super super('ng1Hero', elementRef, injector); } } // This NgModule represents the Angular pieces of the application @NgModule({ declarations: [Ng2HeroesComponent, Ng1HeroComponentWrapper], providers: [ HeroesService, // Register an Angular provider whose value is the "upgraded" AngularJS service {provide: TextFormatter, useFactory: (i: any) => i.get('textFormatter'), deps: ['$injector']} ], // All components that are to be "downgraded" must be declared as `entryComponents` entryComponents: [Ng2HeroesComponent], // We must import `UpgradeModule` to get access to the AngularJS core services imports: [BrowserModule, UpgradeModule] }) export class Ng2AppModule { constructor(private upgrade: UpgradeModule) {} ngDoBootstrap() { // We bootstrap the AngularJS app. this.upgrade.bootstrap(document.body, [ng1AppModule.name]); } } // This Angular 1 module represents the AngularJS pieces of the application export const ng1AppModule: ng.IModule = angular.module('ng1AppModule', []); // This AngularJS component will be "upgraded" to be used in Angular ng1AppModule.component('ng1Hero', { bindings: {hero: '<', onRemove: '&'}, transclude: true, template: `<div class="title" ng-transclude></div> <h2>{{ $ctrl.hero.name }}</h2> <p>{{ $ctrl.hero.description }}</p> <button ng-click="$ctrl.onRemove()">Remove</button>` }); // This AngularJS service will be "upgraded" to be used in Angular ng1AppModule.service('textFormatter', [TextFormatter]); // Register an AngularJS service, whose value is the "downgraded" Angular injectable. ng1AppModule.factory('heroesService', downgradeInjectable(HeroesService) as any); // This directive will act as the interface to the "downgraded" Angular component ng1AppModule.directive('ng2Heroes', downgradeComponent({component: Ng2HeroesComponent})); // This is our top level application component ng1AppModule.component('exampleApp', { // We inject the "downgraded" HeroesService into this AngularJS component // (We don't need the `HeroesService` type for AngularJS DI - it just helps with TypeScript // compilation) controller: [ 'heroesService', function(heroesService: HeroesService) { this.heroesService = heroesService; } ], // This template makes use of the downgraded `ng2-heroes` component // Note that because its element is compiled by AngularJS we must use kebab-case attributes // for inputs and outputs template: `<link rel="stylesheet" href="./styles.css"> <ng2-heroes [heroes]="$ctrl.heroesService.heroes" (add-hero)="$ctrl.heroesService.addHero()" (remove-hero)="$ctrl.heroesService.removeHero($event)"> <h1>Heroes</h1> <p class="extra">There are {{ $ctrl.heroesService.heroes.length }} heroes.</p> </ng2-heroes>` }); // We bootstrap the Angular module as we would do in a normal Angular app. // (We are using the dynamic browser platform as this example has not been compiled AOT.) platformBrowserDynamic().bootstrapModule(Ng2AppModule);
© 2010–2020 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://v10.angular.io/api/upgrade/static/testing/createAngularTestingModule