Ionicにユニットテスト(Jest)の環境を作る

Ionic CLIでプロジェクトを作ると**なぜか**ユニットテスト環境が用意されません。実はこれ昔からそうで、いつか用意されるだろうと思って2年ぐらい経ちましたが全くそういう気配がありません。
じゃあAngular CLIで用意されるKarmaの環境を持ち込もうと思ってもボイラープレートがやたら多くて勘弁してくれよとなります。(個人の感想)

なので色々調べたらJestの環境がさくっと作れるっぽいのでやってみました。

今回のリポジトリは https://github.com/ovrmrw/ionic-super-template-pwa-jest-prettier です。(余談ですがPrettierの設定も組み込んでいます)

多分早ければ**1分くらい**で環境作れます。

# Ionic CLIでプロジェクトを作る

`ionic start`コマンドでプロジェクトを作ります。

```
$ ionic start myApp super
```

これでSuper Starterというてんこ盛りのテンプレートからプロジェクトが作成されます。

# Jestをインストールする

```
$ cd myApp
$ npm install -D jest-preset-angular jest @types/jest
```

この`jest-preset-angular`というやつがいい感じに色々やってくれます。

# Jestの設定ファイルを作る

プロジェクトのルートディレクトリに作ります。おまじないみたいなものです。

```js:jest.config.js
module.exports = {
  preset: 'jest-preset-angular',
  roots: ['src'],
  testRegex: '\\.spec\\.ts$',
  setupTestFrameworkScriptFile: '<rootDir>/src/setupJest.ts',
  transformIgnorePatterns: ['node_modules/(?!@ngrx|@ionic-native|@ionic)']
};
```

# Jestの実行時に読み込むファイルを作る

これらのファイルはsrcディレクトリの直下に作ります。おまじないみたいなものです。

```ts:src/setupJest.ts
import 'jest-preset-angular';
import './jestGlobalMocks'; // browser mocks globally available for every test
```

```ts:src/jestGlobalMocks.ts
const mock = () => {
  let storage = {};
  return {
    getItem: key => (key in storage ? storage[key] : null),
    setItem: (key, value) => (storage[key] = value || ''),
    removeItem: key => delete storage[key],
    clear: () => (storage = {})
  };
};

Object.defineProperty(window, 'localStorage', { value: mock() });
Object.defineProperty(window, 'sessionStorage', { value: mock() });
Object.defineProperty(window, 'getComputedStyle', {
  value: () => ['-webkit-appearance']
});
```

```json:src/tsconfig.spec.json
{
  "extends": "../tsconfig.json",
  "include": [
    "**/*.spec.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}
```

# package.jsonを編集する

テストを実行するためのコマンドを用意します。

```json:package.json
  (省略)
  "scripts": {
    (省略)
    "test": "jest",
    "test:w": "jest --watch",
    "test:ci": "jest --runInBand"
  },
  (省略)
```

# 環境構築終わり

Jestによるユニットテストの環境構築はこれで全てです。
コードは全てコピペでOKなので手間と言えるほどの作業は一切ありません。

※おまじないファイルを一括生成するスクリプトも用意しました。
 https://qiita.com/ovrmrw/items/a25a229da02a6dbe2603#%E8%BF%BD%E8%A8%98

# Serviceのテストを書いてみた

[simple.service.spec.ts](https://github.com/ovrmrw/ionic-super-template-pwa-jest-prettier/blob/master/src/providers/test-example/simple.service.spec.ts) より

```ts:simple.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { SimpleService } from './simple.service';

describe('SimpleService', () => {
  beforeEach(() =>
    TestBed.configureTestingModule({
      providers: [SimpleService]
    })
  );

  describe('with injection', () => {
    let simpleService: SimpleService;

    beforeEach(() => {
      simpleService = TestBed.get(SimpleService);
    });

    it('add', () => {
      const expected = 3;
      const actual = simpleService.add(1, 2);
      expect(actual).toBe(expected);
    });

    it('subtract', () => {
      const expected = -1;
      const actual = simpleService.subtract(1, 2);
      expect(actual).toBe(expected);
    });
  });
});
```

`add()`と`subtract()`という関数を持つServiceの簡単なテストです。

# Component(Page)のテストを書いてみた

[welcome.spec.ts](https://github.com/ovrmrw/ionic-super-template-pwa-jest-prettier/blob/master/src/pages/welcome/welcome.spec.ts) より

```ts:welcome.spec.ts
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { IonicModule, NavController } from 'ionic-angular';
import { WelcomePage } from './welcome';

const mockNavController = {
  push: jest.fn()
};

describe('WelcomePage', () => {
  beforeEach(() =>
    TestBed.configureTestingModule({
      declarations: [WelcomePage],
      imports: [IonicModule.forRoot(WelcomePage), TranslateModule.forRoot()],
      providers: [{ provide: NavController, useValue: mockNavController }],
      schemas: [NO_ERRORS_SCHEMA]
    }).compileComponents()
  );

  describe('with injection', () => {
    let fixture: ComponentFixture<WelcomePage>;
    let welcomePage: WelcomePage;

    beforeEach(() => {
      fixture = TestBed.createComponent(WelcomePage);
      welcomePage = fixture.componentInstance;
    });

    afterEach(() => {
      jest.resetAllMocks();
    });

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

    it('when login function is called.', () => {
      welcomePage.login();
      expect(mockNavController.push.mock.calls.length).toBe(1);
      expect(mockNavController.push.mock.calls[0]).toEqual(['LoginPage']);
    });

    it('when signup function is called.', () => {
      welcomePage.signup();
      expect(mockNavController.push.mock.calls.length).toBe(1);
      expect(mockNavController.push.mock.calls[0]).toEqual(['SignupPage']);
    });
  });
});
```

ComponentのテストはServiceより記述量が多くなりますが、やっていることは

- インジェクトしているService(NavController)をモックする。
- DIコンテナからComponentのインスタンスを取得してテストする。
    - インスタンスが生成されていることの確認。
    - NavContollerの`push()`が指定した引数を伴って呼ばれていることの確認。

これだけです。

# まとめ

JestでComponentのテストもできることがわかりました。
環境構築はあっという間にできるのでIonic CLIがやってくれるとありがたいのですが。

※全体的に参考にしたのは https://github.com/datencia/ionic2-jest-example です。

# 追記

おまじないファイルを一括生成するスクリプトを用意しました。

https://gist.github.com/ovrmrw/57f8e02f5776656825f8db8867f03578

上記のJSファイルのコードをコピーして、プロジェクトのルートディレクトリで

```
$ node
> (←入力を受け付ける状態になる)
```

ここでコードを貼り付けるとスクリプトが実行されてファイルが生成されます。楽ちんですね。