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系環境でユニットテスト環境の構築と、テストコードの作成方針を簡単にまとめました。
今触っているプロジェクトベースで書いているので網羅的にはなっていないと思いますが少しでも参考になればと思います。