Angular 8.1.0 から入った createAngularJSTestingModule を読んでみる

この記事は [Angular Advent Calendar 2019](https://qiita.com/advent-calendar/2019/angular) 19日目の記事です。

# はじめに
[Angular 9.0のリリースが2020年になる](https://twitter.com/jules_kremer/status/1206645952548720640)なんて話がつい先日ありましたが、今もAngularJSが現役で動いているプロジェクトはまだまだあるんじゃないでしょうか。  
僕が参加しているプロジェクトもその中のひとつで、現在はAngularJS + Angular 8のハイブリッド構成で動いています。今も絶賛移行中です:sob:

さて、Angular 8.1.0のリリースで `createAngularJSTestingModule` と `createAngularTestingModule` がリリースされたことをご存知でしょうか？

AngularJSとAngularがハイブリッドで動いている環境での、ユニットテストをサポートするためのHelper functionなわけですが
今日はその中の `createAngularJSTestingModule` について見ていこうと思います :eyes: 

うちのチームで動かしているハイブリッドアプリケーションのテストがたまに通らないことがあり、そもそもテストではどんなことをやってるのかを見てみたかったというのがキッカケです。


# ざっくり使いどころを説明

Angularで書かれたServiceをdowngradeしてAngularJSで使う場合、
テストの中でもハイブリッドのアプリケーションをbootstrapしてあげる必要がありました。
しかし、新しく追加された[createAngularJSTestingModule](https://angular.io/api/upgrade/static/testing/createAngularJSTestingModule)を使うことでその必要がなくなります。テストがシンプルに書けるし、高速に実行できるようになるようです。

テストの中で

```ts
beforeEach(module(createAngularJSTestingModule([Ng2AppModule])));
beforeEach(module(ng1AppModule.name));
```

と書けば、AngularJSのInjectorからAngularのServiceを取り出すことができるようになります。こんな感じ↓↓

```ts
it('should have access to the HeroesService',
  inject((heroesService: HeroesService) => { 
    expect(heroesService).toBeDefined();
  })
);
```

# コードを見てみる
[Angular 8.2.14の@angular/upgrade/static/testingにあるcreateAngularJSTestingModule](https://github.com/angular/angular/blob/8.2.14/packages/upgrade/static/testing/src/create_angularjs_testing_module.ts#L15-L100) のソースです。

```ts
export function createAngularJSTestingModule(angularModules: any[]): string {
  return ng.module_('$$angularJSTestingModule', [])
      .constant(UPGRADE_APP_TYPE_KEY, UpgradeAppType.Static) // '$$angularUpgradeAppType', 2
      .factory(
          INJECTOR_KEY, // '$$angularInjector'
          [
            $INJECTOR,  // '$injector'
            ($injector: ng.IInjectorService) => {
              TestBed.configureTestingModule({
                imports: angularModules,
                providers: [{provide: $INJECTOR, useValue: $injector}]
              });
              return TestBed.get(Injector);
            }
          ])
      .name;
}
```

ここでやっていることは
- `$$angularJSTestingModule` という AngularJSのモジュールを作成する
- `$$angularUpgradeAppType` という AngularJSの constant に 2(UpgradeAppType.Static) を指定している
- `TestBed.configureTestingModule` でテスト用のモジュールを生成しつつ…
- `$$angularInjector` という名前で Angularの `Injector` をAngularJS に提供している

です。これだけでは「AngularJSから `$$angularInjector` という名前でDIしてないぞ」と思うかもしれませんが
次にお見せする `downgradeInjectable` が鍵となります。
`downgradeInjectable` は Angular の Service を AngularJSで使うために必要になるヘルパー関数で

```ts
@Injectable()
export class AwesomeService { ... }

ng1App.factory('AwesomeService', downgradeInjectable(AwesomeService) as any)
```

という感じで書きます。それを踏まえて [`downgradeInjectable` の実装](https://github.com/angular/angular/blob/master/packages/upgrade/src/common/src/downgrade_injectable.ts)を見に行くと…

```ts
// 第２引数の downgradedModule は使っていないです
export function downgradeInjectable(token: any, downgradedModule: string = ''): Function {
  const factory = function($injector: IInjectorService) {
    const injectorKey = `${INJECTOR_KEY}${downgradedModule}`; // '$$angularInjector'

    // isFunction = (t) => typeof t === 'function'
    // なので、ここではgetTypeNameの結果 'AwesomeService' が使われます(ログ用)
    const injectableName = isFunction(token) ? getTypeName(token) : String(token);
    const attemptedAction = `instantiating injectable '${injectableName}'`;

    // ただしくupgradeされているアプリケーションかチェックする（雑に書いてます）
    // createAngularJSTestingModule内で '$$angularUpgradeAppType' を 2 として定義したのは
    // ここのチェックを通すためだと思われる。本来であればbootstrapをしないと通せない場所。
    validateInjectionKey($injector, downgradedModule, injectorKey, attemptedAction);

    // AngularJSの世界に定義してあるAngularのInjectorを取得する
    const injector: Injector = $injector.get(injectorKey);
    return injector.get(token); // そこからAwesomeServiceを取得して返す
  };
  (factory as any)['$inject'] = [$INJECTOR];

  return factory;
}
```

という感じになっています。
Angularの `downgradeInjectable` ではアプリケーションのbootstrapが済んでいるかをチェックしており
ここをパスするための必要最低限のことを行っていることがわかりました :bulb: 


# あとがき
本当であれば、業務で使っているコードをいい感じのサンプルプロジェクトに落とし込むのがいいんですが、うまくテストが動かない部分もあり断念しました :sob:  

Angularのバージョンがもうすぐ9になろうとしているのに、Angularチームは継続してAngularJSからのマイグレーションをサポートするためのツールをリリースしてくれました。感謝しかないです :pray: