この記事は Angular Advent Calendar 2019 19日目の記事です。
はじめに
Angular 9.0のリリースが2020年になるなんて話がつい先日ありましたが、今もAngularJSが現役で動いているプロジェクトはまだまだあるんじゃないでしょうか。
僕が参加しているプロジェクトもその中のひとつで、現在はAngularJS + Angular 8のハイブリッド構成で動いています。今も絶賛移行中です
さて、Angular 8.1.0のリリースで createAngularJSTestingModule
と createAngularTestingModule
がリリースされたことをご存知でしょうか?
AngularJSとAngularがハイブリッドで動いている環境での、ユニットテストをサポートするためのHelper functionなわけですが
今日はその中の createAngularJSTestingModule
について見ていこうと思います
うちのチームで動かしているハイブリッドアプリケーションのテストがたまに通らないことがあり、そもそもテストではどんなことをやってるのかを見てみたかったというのがキッカケです。
ざっくり使いどころを説明
Angularで書かれたServiceをdowngradeしてAngularJSで使う場合、
テストの中でもハイブリッドのアプリケーションをbootstrapしてあげる必要がありました。
しかし、新しく追加されたcreateAngularJSTestingModuleを使うことでその必要がなくなります。テストがシンプルに書けるし、高速に実行できるようになるようです。
テストの中で
beforeEach(module(createAngularJSTestingModule([Ng2AppModule])));
beforeEach(module(ng1AppModule.name));
と書けば、AngularJSのInjectorからAngularのServiceを取り出すことができるようになります。こんな感じ↓↓
it('should have access to the HeroesService',
inject((heroesService: HeroesService) => {
expect(heroesService).toBeDefined();
})
);
コードを見てみる
Angular 8.2.14の@angular/upgrade/static/testingにあるcreateAngularJSTestingModule のソースです。
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で使うために必要になるヘルパー関数で
@Injectable()
export class AwesomeService { ... }
ng1App.factory('AwesomeService', downgradeInjectable(AwesomeService) as any)
という感じで書きます。それを踏まえて downgradeInjectable
の実装を見に行くと…
// 第2引数の 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が済んでいるかをチェックしており
ここをパスするための必要最低限のことを行っていることがわかりました
あとがき
本当であれば、業務で使っているコードをいい感じのサンプルプロジェクトに落とし込むのがいいんですが、うまくテストが動かない部分もあり断念しました
Angularのバージョンがもうすぐ9になろうとしているのに、Angularチームは継続してAngularJSからのマイグレーションをサポートするためのツールをリリースしてくれました。感謝しかないです