AngularでJestを用いたスナップショットテストをCIで回してみる(前編)

# この記事は？
UIコンポーネントを作成時に、スナップショットテストを試して見たかったので
Angular、Jest、GithubActionsを用いたCI構築の手順を紹介します。
この記事では、AngularとJestを使用したスナップショットテストの手順までを紹介します。

# 対象の読者は？
- Jestが初めての方
- スナップショットテストって何ぞやって方

# スナップショットテストとは？
>スナップショットテストとはテストの対象になっている関数の出力をスナップショットとして保存し
コードの変更によってスナップショットに変更が生じているかどうかを検出（テスト）します。
特にUIコンポーネントなど、UIのテストを重点的に行いたい場合に有用な方法です。

jestでのスナップショットテストの公式ドキュメントは以下となっています。
詳しい内容については、後続のjestを用いたスナップショットテストの章で説明します。
https://jestjs.io/docs/ja/snapshot-testing

# セットアップ
## Angularの環境を用意する
詳しい方法はAngularの公式ドキュメントを参照してください
https://angular.jp/guide/setup-local

```bash

# AngularCLIをinstall
$ npm install -g @angular/cli

# プロジェクトを作成
$ ng new ${プロジェクト名}

# アプリケーションの実行
$ ng serve --open
```

成功するとお馴染みの画面が表示されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/160148/89a4c12d-5519-02aa-2389-4a6edf721563.png)

# Jestのインストール
AngularCLIを使ってプロジェクトを作成すると、テストツールとしてkarmaが初期で設定されています。
そのため、Jestをインストール後にkarmaの設定を削除する必要があります。
OSSでjestの設定を行ってくれるツールを今回は利用して、各種設定を行います。
https://github.com/briebug/jest-schematic

```bash

$ ng add @briebug/jest-schematic
$ npm install -g @briebug/jest-schematic
$ ng g @briebug/jest-schematic:add

Installing packages for tooling via npm.
Installed packages for tooling via npm.
DELETE karma.conf.js
DELETE src/test.ts
CREATE jest.config.js (180 bytes)
CREATE setup-jest.ts (860 bytes)
CREATE test-config.helper.ts (611 bytes)
UPDATE package.json (1199 bytes)
UPDATE angular.json (3566 bytes)
UPDATE tsconfig.spec.json (316 bytes)
✔ Packages installed successfully.
```

上記コマンドにより
Jestがインストールされ、設定ファイルが追加されます。
また、不必要となったKarmaとJasmineの設定ファイルを両方とも削除してくれます。

試しにテストを実行して見ましょう。

```bash

$ npm test

 PASS  src/app/app.component.spec.ts
  AppComponent
    ✓ should create the app (290 ms)
    ✓ should have as title 'SnapShotTest' (172 ms)
    ✓ should render title (103 ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        5.035 s
Ran all test suites.
```

問題なく動いてますね。これでjestを使う準備は完了です。

# jestを用いたスナップショットテスト

## スナップショットテストは何故生まれたのか？
詳細については下記のリリースノートに書かれているので参照してください。
ここでは、一部だけ抜粋します。
https://jestjs.io/blog/2016/07/27/jest-14.html

> We want to make it as frictionless as possible to write good tests that are useful. We observed that when engineers are provided with ready-to-use tools, they end up writing more tests, which in turn results in stable and healthy code bases.

有用な良いテストを書くためにできるだけ摩擦のないようにしたいと考えています。
エンジニアがすぐに使えるツールを提供されると
より多くのテストを書くことになり
結果的に安定した健全なコードベースになることがわかりました。

> engineers frequently told us that they spend more time writing a test than the component itself. As a result many people stopped writing tests altogether which eventually led to instabilities. Engineers told us all they wanted was to make sure their components don't change unexpectedly
by. Christoph Nakazawa

従来のフロントエンド開発では
エンジニアはコンポーネントそのものの開発よりも
テストを書くことに時間を費やしていることが頻繁に起きています。
これは多くの人がテストを書かなくなることに繋がり
最終的には不安定なコードが出来上がってしまう危険性があります。
開発者の最終的な目的は、コンポーネントが予期せぬ変化を起こしていないことを
確認したいという思想からスナップショットテストが生まれています。

ふむふむ。凄く良さそう👀

## 試しに使ってみる
### スナップショットの作成
以下のシンプルなcomponentに対してスナップショットテストを実行してみます。

``` app.component.html
<h1>snap shot test!!!</h1>
```

```app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'SnapShotTest';
}
```

```app.component.spec.ts
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';

describe('AppComponent', () => {
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
    }).compileComponents();
  });

  it('should create the app', () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.componentInstance;
    expect(app).toBeTruthy();
  });

  it('should have as title SnapShotTest', () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.componentInstance;
    expect(app.title).toEqual('SnapShotTest');
  });

  // スナップショットテスト
  it('snap shot test', () => {
    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    const compiled = fixture.nativeElement;
    // テスト対象のnativeElementのスナップショットを作成
    expect(compiled).toMatchSnapshot();
  });
});
```

テストの作成は非常に簡単で
テスト対象に対してtoMatchSnapshot()メソッドを呼び出すだけです。

jestのスナップショットテストでは
初回実行時にスナップショットを保持するためのディレクトリ・ファイルが作成されます。
上記のテストを実行すると以下のような構成となります。

```bash
$ tree src/app 
src/app
├── __snapshots__ // new!!
│   └── app.component.spec.ts.snap // new!!
├── app.component.css
├── app.component.html
├── app.component.spec.ts
├── app.component.ts
└── app.module.ts
```
app配下にsnapshotsと言うディレクトリが出来てますね。
新規で作成されたapp.component.spec.ts.snapの中身も覗いて見ます👀

```app.component.spec.ts.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`AppComponent snap shot test 1`] = `
<div
  id="root1"
>
  <h1>
    snap shot test!!!
  </h1>
</div>
`;
```

どうやら先ほどのテストで対象として指定した
nativeElementの値がファイルに格納されていることが分かります。
jestはこのスナップショットのファイルをテスト実行時に参照することにより
以前のスナップショットと構造の変化がないかを検出(テスト)するみたいです。

### テストを落としてみる
試しに先ほどのテストを落として見ましょう。
今回はシンプルにh1タグで定義していたタグをh2に変更して動作を確認してみます。

```app.component.html
<!--変更前-->
<h1>snap shot test!!!</h1>
```

```app.component.html
<!--変更後-->
<h2>snap shot test!!!</h2>
```

テストを実行すると下記の結果が得られます。
jestはh1 -> h2にタグの状態が変化したことを検知しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/160148/d21253b3-b4d6-15f7-7bcc-684f70f88c75.png)

かなりお手軽にDomの変化を検出できました🎉
jestのスナップショットの思想通り
開発者は導入するだけでUTの負担軽減や抜け漏れを検出することが出来そうです。

### 落ちたテストを再度通してみる
先ほどは意図通りDomの変更を検出し、テストをFailさせることが出来ました。
もしテスト結果が意図通りの変化なら
開発者はスナップショットを変更しテストを通す必要があります。
jestでは--updateSnapshotコマンドを使用することにより
snapshotファイルを更新することが出来ます。

```bash
$ jest --updateSnapshot
```
更新後のsnapshotのファイルを覗いて見ます👀

```app.component.spec.ts.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`AppComponent snap shot test 1`] = `
<div
  id="root2"
>
  <h2> <- 意図通り書き換わってる！！
    snap shot test!!!
  </h2>
</div>
`;
```
上記のようにスナップショットのファイルが更新されていることが分かります。
この状態で再度テストを実行すると、無事テストが通っていることを確認できました。

```
$ npm test
PASS  src/app/app.component.spec.ts
  AppComponent
    ✓ should create the app (70 ms)
    ✓ should have as title 'SnapShotTest' (15 ms)
    ✓ snap shot test (18 ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   1 passed, 1 total
Time:        2.9 s
Ran all test suites.
```

スナップショットファイルの更新対象は--testNamePatternで指定することも可能みたいです。

### 最後に
感想として、インタフェースがシンプルで使い勝手も良さそうだと感じました。
UIコンポーネントのCIに組み込めばかなり恩恵は受けられそうな感じがします。
後編ではGithubActionsを利用して、上記のテストをCIで回す方法を書きます。

# 参考
- https://jestjs.io/docs/ja/snapshot-testing#
- https://tarosky.co.jp/tarog/4662#