Ionic(Angular)アプリにユニットテストを導入する　サービス編

前回Ionicアプリ(Angular)のユニットテストの作成について軽く触れました。
[Qiita - Ionicアプリにユニットテストを導入する
](https://qiita.com/yunity29/items/a71262b7c55021504ed6)
今回はサービスクラスのテスト方法について記述していきます。
Ionicはサンプルプロジェクトで使っているだけで、内容はほぼAngular+jasmineのテスト方法です。

この記事のサンプルコードは[GitHub - ionic-test-sample](https://github.com/yunity29/ionic-test-sample/tree/master/src/app/provider)に置いています。

##環境
- Ionic 5.0.0
- Angular 9.1.6
- jasmine 3.5.0
- Karma 5.0.0


##他のサービスに依存しないサービス
`dateService`クラスに受け取った`Date`オブジェクトの西暦年を返却するメソッドを作成しました。
他のサービス依存しない独立したサービスです。独立したサービスのテストは容易に作成可能です。


テスト用モジュールにテスト対象のサービスを注入してテストします。
テスト用モジュールを使用せず`beforeEach`でインスタンス化するだけでもテスト可能です。

```data.service.ts
export class DateService {

  constructor() { }

  public getYear(date: Date) {

    return date.getFullYear();

  }
}

```

```data.service.spec.ts
describe('DateService', () => {
  let service: DateService;

  beforeEach(() => {

    // テスト環境の作成とサービスの注入
    TestBed.configureTestingModule({});
    service = TestBed.inject(DateService);
  });

  describe('getYear', () => {
    
    // テスト部分。実行結果と期待値を比較する
    it(`'2018/5/9' 2018 `, () => {
      expect(service.getYear(new Date('2018/5/9'))).toEqual(2018);
    });
  }
}
```

###非同期の場合
テスト対象のメソッドは先ほどと同じく西暦を取得する目的ですが、今回は非同期で取得します。

```date.service.ts
export class DateService {

  constructor() { }
  
  // 約1秒後にresolveを実行して呼び出し元に通知する。
  public getYearAsync(date: Date) {

    return new Promise(resolve => {
      setTimeout(() => {
        resolve(date.getFullYear());
      }, 1000);
    });
  }
}
```

非同期の場合、`await`でPromiseの状態を解決を待ってから、結果と期待値を比較します。
JasmineのドキュメントではPromiseでのコーディング方法も記載されていますが簡潔に書ける`await`が望ましいでしょう。
[Jasmine公式 - Asynchronous Work](https://jasmine.github.io/tutorials/async)

```date.service.spec.ts
describe('DateService', () => {
  let service: DateService;

  beforeEach(() => {

    // テスト環境の作成とサービスの注入
    TestBed.configureTestingModule({});
    service = TestBed.inject(DateService);
  });

  describe('getYearAsync', () => {
    
    // テスト部分。awaitでgetYearAsyncのresolveの実行を待つ。
    it(`'2018/5/9' 2018 `, async () => {
      const result = await service.getYearAsync(new Date('2018/5/9'));
      expect(result).toEqual(2018);
    });
  }
}
```

##他のサービスに依存する場合
他のサービスに依存するという状態はよくあります。
依存するサービスが未完成であるなど、テスト対象のサービスを独立してテストを行いたい場合は、スタブを利用して、テスト毎に依存するサービスの固定値を返却するようにします。

スタブとは→ [Qiita - スタブとモックの違い @k5trismegistus ](https://qiita.com/k5trismegistus/items/10ce381d29ab62ca0ea6)

今回のサンプルコードでは、令和時代の最初のオリンピック開催年を取得するメソッドをテストします。2020年を返却する想定です。
※コロナによる延期はなかった世界線の話です。。。

![名称未設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/537165/8c02c018-b48c-e827-0e2b-00cde0f067dc.png)

別のサービスである`dateService のgetFirstReiwaYear`を使用して、令和元年の西暦を取得します。


```olympic.service.ts
export class OlympicService {
  constructor(private dateService: DateService) { }

  public getFirstReiwaOlympicYear() {
    
    const firstReiwaYear = this.dateService.getFirstReiwaYear();

    for (let i = 0;true;i++) {
      if ((firstReiwaYear + i) % 4 === 0) {
        return firstReiwaYear + i;
      }
    }
  }
}
```

ところが`dateService のgetFirstReiwaYear`はまだ作成されていないので、スタブを利用して固定値を返却するようにテストコードを作成します。実体の`getFirstReiwaYear`ではテキトーに0を返却しています。


```data.service.ts
export class DateService {

  constructor() { }

  public getFirstReiwaYear():number {

    // TODO 実装途中
    return 0;
  }
```

Jasmineのスパイという機能を使ってスタブ利用しています。スパイオブジェクトに`DateService`を代替させています。
これによって`stubValue`に代入した値がスタブの返却値としてテストを実行します。

本来はテキトーに返却している`0`を使うはずですが、固定値である`2019`を利用してテストできています。
サンプルコードはAngular公式の[テストのページ](https://angular.jp/guide/testing)をかなり参考にしました。

```olympic.service.ts
describe('OlympicService', () => {
  let service: OlympicService;

  describe('getFirstReiwaOlympicYear', () => {
    let olympicService: OlympicService;
    let dateServiceSpy: jasmine.SpyObj<DateService>;

    beforeEach(() => {
      const spy = jasmine.createSpyObj('DateService', ['getFirstReiwaYear']);

      TestBed.configureTestingModule({
        providers: [
          { provide: DateService, useValue: spy }
        ]
      });
      olympicService = TestBed.inject(OlympicService);
      dateServiceSpy = TestBed.inject(DateService) as jasmine.SpyObj<DateService>;;

    });

    // getFirstReiwaYearの返却値を2019に設定してテストを実行します。
    it(`stab:2019 toEqual(2020)`, () => {
      const stubValue = 2019;
      dateServiceSpy.getFirstReiwaYear.and.returnValue(stubValue);

      expect(olympicService.getFirstReiwaOlympicYear()).toEqual(2020);
    });

  });
});
```

##おわりに
http通信のテストもサービステストしての役割が大きいので追記していきたいと思います。
この記事のサンプルコードは下のリポジトリを入れています。
[GitHub - ionic-test-sample](https://github.com/yunity29/ionic-test-sample/tree/master/src/app/provider)

サービスクラスのテストはコンポーネントのテストと比べて作成コストが低く壊れづらいので、優先してサービスのテストコードを作成するのが良いかと思います。
Angularのドキュメントにはロジックはサービスに書くことを推奨してます。この指針を意識してプロダクトの開発を進めていけば、テストコードの作成コストも自ずと下がっていきます。