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を動かせるようにする
色々ハマりましたが、最終的にはこちらの記事を参考にしつつ、
- プロダクトコードは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さんのモダンプラクティスや@laco0416さんのAngularJS老化チェックの書き方に則っていればテスタビリティも確保できると思います。
課題(というかまだ見えてないこと)
-
compileやlink関数内で動的に出力するtemplateを差し替えていたりする場合のテストをどうするか
Controller
-
$scopeなどに依存してない部分は普通にテスト書く -
$scope.$watchなどに依存している場合-
$watchに登録したコールバック関数の処理が単純なら振る舞いをそのままテストする - 複雑な場合は、コールバック関数をプロパティなどにして、
$watchの発火のテストとコールバック関数についてのテストを分割する(この辺参照)- ここでarrow function使っておかないとthisの扱いで爆死するので注意
- そもそもそんな複雑なことやるなという話
- テストを書いたら、
$scope依存は消せそうか検討して極力消す。
-
-
$scopeのプロパティを利用していた場合- 全て
Controllerのプロパティへ移植する -
Directiveに紐づいていた場合はbindToControllerを利用
- 全て
Service,Factory
-
$http,$resoucesに依存している場合-
$httpBackendを利用してAPIレスポンスをモックしてテスト
-
- それ以外は普通にテスト書くだけ(多分
Filter
ただの関数のはずなので特別なことあんまりする必要ないと思ってます
おわりに
ということでTypeScript + Angular1系環境でユニットテスト環境の構築と、テストコードの作成方針を簡単にまとめました。
今触っているプロジェクトベースで書いているので網羅的にはなっていないと思いますが少しでも参考になればと思います。