[Angular, Jest] TypeError: Cannot read property 'ɵprov' of undefined のエラーを解決


要約

Angularのサービスクラスに対するテストコードを作っていたとき、以下のエラーでハマりました。

TypeError: Cannot read property 'ɵprov' of undefined

または

Can't resolve all parameters for HeroService: (?).

結論から言うと、原因は分かっていないのですが、サービスクラスの@Injectable内部の記述を以下のように変更することで解消しました。

@Injectable({ 
-  providedIn: 'root',
+  providedIn: 'root'
})
export class HeroService {

カンマ消しただけです・・・。
原因分かる方がいらっしゃったら、コメント欄で教えていただけると幸いです。

(2020.11.15 追記)
解決しました。(やはりカンマは関係ありませんでした。)
tsconfigcompilerOptionsに以下のパラメータを追加したらエラーが解消されました。

tsconfig.json
{
  "compilerOptions": {
+    "emitDecoratorMetadata": true
  },
}

こちらのjest-preset-angularによると、Angular8以上はCLIの設定によってメタデータが欠ける可能性があるそうです。

With Angular 8 and higher, a change to the way the Angular CLI works may be causing your metadata to be lost

この設定を追加して実行すると、テストが通りました。
ちなみに、設定を変更したのに変わらない場合は、キャッシュを無効にして実行すると良いかもしれません。

$ jest hero.service --no-cache

(追記おわり)


環境

$ ng --version
Angular CLI: 10.1.7
Node: 12.16.1
OS: win32 x64

Angular: 10.1.6
... animations, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, router
Ivy Workspace: Yes

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1001.7
@angular-devkit/build-angular   0.1002.0
@angular-devkit/core            10.1.7
@angular-devkit/schematics      10.1.7
@angular/cli                    10.1.7
@schematics/angular             10.1.7
@schematics/update              0.1001.7
rxjs                            6.6.3
typescript                      4.0.5

経緯

Angularのチュートリアル「Tour Of Heroes」の「4. サービスの追加」にあるサンプルコードに対してテストコードを実装しようとしていました。

テスト対象のクラスは以下。ダウンロードしたファイルそのままです。

hero.service.ts
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
import { MessageService } from './message.service';

@Injectable({
  providedIn: 'root',
})
export class HeroService {

  constructor(private messageService: MessageService) { }

  getHeroes(): Observable<Hero[]> {
    // TODO: send the message _after_ fetching the heroes
    this.messageService.add('HeroService: fetched heroes');
    return of(HEROES);
  }
}

それに対して、以下のようなテストコードを実装。

hero.service.spec.ts

import { TestBed } from '@angular/core/testing';
import { HeroService } from 'src/app/hero.service';

describe('TestHeroService', () => {
    let heroService: HeroService;

    beforeEach(() => {
        TestBed.configureTestingModule({});
        heroService = TestBed.inject(HeroService);
    });

    it('should be created', () => {
        expect(heroService).toBeTruthy();
    });
});

インスタンス化されることをサクッとテストして、メソッドのテストを書くつもりだったのですが、冒頭の通り

TypeError: Cannot read property 'ɵprov' of undefined

というエラーが発生しました。
調べてみると正しくDIができていないようなのですが、はっきりとした原因は分かりませんでした。

解決への道のり

以下のような経緯でとりあえず解決しました。

  1. HeroServiceにおいて、MessageServiceをDIしないようにコメントアウトした。
  2. エラーが解消したので、やはりDIが関係していると判断
  3. Angular CLIを用いて別のサービス(TestService)を作成。HeroServiceと同様にMessageServiceをDIした上で同様のテストコードを作成。
  4. TestServiceのテストコードが通った。
  5. HeroServiceとTestServiceを比較して、providedInの部分が異なることを発見し、修正したらHeroServiceのテストコードが通った。

修正後のコードはこちらです。
hero.service.spec.ts
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
import { MessageService } from './message.service';

@Injectable({
  providedIn: 'root' // カンマを消しただけ
})
export class HeroService {

  constructor(private messageService: MessageService) { }

  getHeroes(): Observable<Hero[]> {
    // TODO: send the message _after_ fetching the heroes
    this.messageService.add('HeroService: fetched heroes');
    return of(HEROES);
  }
}