TypeScript + Angular1系プロジェクトにユニットテストを書く

Angular1系で書かれてるプロダクトにテストを用意するにあたって環境の構築、実際のテストコードの作成など、色々調べたのでまとめていこうと思います。


# 前提環境
- TypeScript（v1.8, 含テストコード）
- AngularJS(v1.4)
- karma+mocha+power-assert

コードはイマドキのrequireからの依存解決してbundleするスタイルではなく、TypeScriptのnamespaceでスコープを区切って`tsc --outFile dist/bundle.js`のようにconcatした単一ファイルを吐き出す形です。

環境を再現したリポジトリ：https://github.com/sisisin-sandbox/ngts/tree/master/namespace-style


# 最低限karmaを動かせるようにする

色々ハマりましたが、最終的には[こちらの記事](http://qiita.com/k_7016/items/fd0cb51770fef96e8e24)を参考にしつつ、

- プロダクトコードはconcat済のものを参照（`karma.conf.js`の`files`には`dist/bundle.js`を読ませる）
- テストコードは`karma-browserify`を利用し、`browserify` + `tsify` + `espowerify`でts -> espoweredなjsにして実行

という形に落ち着きました。
これで、プロダクトコードの型を参照しつつテストコードを書き、アサーションエラー時にはpower-assertの詳細なエラーが確認できます。

# Directiveのテストをkarmaで動かす
Directiveにて`templateUrl`を使ってhtmlを参照していると、下記のようなエラーが表示され、テストが落ちます。

```
    Error: Unexpected request: GET /views/appHelloDirective.html
    No more request expected
        at $httpBackend (/var/folders/0q/d6gvmfsn7gv849kw__r2py_00000gp/T/node_modules/angular-mocks/angular-mocks.js:1418:0 <- /var/folders/0q/d6gvmfsn7gv849kw__r2py_00000gp/T/cd8d66ce58eb2b84e246e72fab23a78f.browserify:1727:9)
        at sendReq (node_modules/angular/angular.js:11776:9)
        at serverRequest (node_modules/angular/angular.js:11571:16)
        at processQueue (node_modules/angular/angular.js:16383:28)
        at node_modules/angular/angular.js:16399:27
        at Scope.$eval (node_modules/angular/angular.js:17682:28)
        at Scope.$digest (node_modules/angular/angular.js:17495:31)
        at Context.<anonymous> (/var/folders/0q/d6gvmfsn7gv849kw__r2py_00000gp/T/test/directive-spec.ts:25:10 <- /var/folders/0q/d6gvmfsn7gv849kw__r2py_00000gp/T/cd8d66ce58eb2b84e246e72fab23a78f.browserify:17408:15)
```

これを回避するために、`karma-ng-html2js-preprocessor`というパッケージを使います。
このパッケージはAngularのtemplateとして書かれたhtmlファイルをjsに変換し、karmaで実行できるようによしなにしてくれます。
また、`templateUrl`のファイルパス解決についてのオプションも用意されており、実行時のパスが異なるディレクトリになっている場合でも柔軟に対応できます。


# テストを書くときの方針
## 全体方針
- Angularに依存している部分は`anuglar-mocks`でモックしつつテストする
    - `Directive`,`$scope`,`$http`など
- DI対象が多い場合やネストしてる場合も`angular-mocks`を利用してやる
- それ以外はなるべく`angular-mocks`を利用しないでjs(ts)オンリーでテストを書く
    - 薄い`Controller`や`$http`を利用しない`Service`,`Factory`など
- テスト用のデータは関数で取得するようにする
    - 例：`const data1 = () => [1, 2, 3];`
    - 生のオブジェクトや配列を`$scope`などに渡すとAngularの方でプロパティ生やされたりするので（`$$hashKey`など）


## Directive
- 極力`attribute`から渡された値が`Controller`にbindされていることを確認するのみに留める
- `link`関数などで盛大にロジックが書かれてしまっている場合
    - 一旦ロジックに対してもテストを書く
    - しかる後にロジックをテストとともに`Controller`に分離して`bindToController`に書き直し、`link`関数を撲滅する

基本的に、@armorik83さんの[モダンプラクティス](http://qiita.com/armorik83/items/5542daed0c408cb9f605)や@laco0416さんの[AngularJS老化チェック](http://qiita.com/laco0416/items/edfa917583af4593ad6c)の書き方に則っていればテスタビリティも確保できると思います。



### 課題（というかまだ見えてないこと）

- `compile`や`link`関数内で動的に出力するtemplateを差し替えていたりする場合のテストをどうするか



## Controller

- `$scope`などに依存してない部分は普通にテスト書く
- `$scope.$watch`などに依存している場合
    - `$watch`に登録したコールバック関数の処理が単純なら振る舞いをそのままテストする
    - 複雑な場合は、コールバック関数をプロパティなどにして、`$watch`の発火のテストとコールバック関数についてのテストを分割する[(この辺参照)](https://github.com/sisisin-sandbox/ngts/blob/master/namespace-style/src/modules/controller.ts#L21)
        - ここでarrow function使っておかないとthisの扱いで爆死するので注意
        - **そもそもそんな複雑なことやるなという話**
    - テストを書いたら、`$scope`依存は消せそうか検討して極力消す。
- `$scope`のプロパティを利用していた場合
    - 全て`Controller`のプロパティへ移植する
    - `Directive`に紐づいていた場合は`bindToController`を利用

## Service,Factory
- `$http`, `$resouces`に依存している場合
    - `$httpBackend`を利用してAPIレスポンスをモックしてテスト
- それ以外は普通にテスト書くだけ（多分


## Filter
ただの関数のはずなので特別なことあんまりする必要ないと思ってます


# おわりに
ということでTypeScript + Angular1系環境でユニットテスト環境の構築と、テストコードの作成方針を簡単にまとめました。
今触っているプロジェクトベースで書いているので網羅的にはなっていないと思いますが少しでも参考になればと思います。