createAngularTestingModule

function

A helper function to use when unit testing Angular services that depend upon upgraded AngularJS services.

See more...

createAngularTestingModule(angularJSModules: string[], strictDi?: boolean): Type<any>

Parameters
angularJSModules string[]

a collection of the names of AngularJS modules to include in the configuration.

strictDi boolean

whether the AngularJS injector should have strictDI enabled.

Optional. Default is undefined.

Returns

Type<any>

Description

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 or downgradeModule 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.

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