Angular でテストコードの書き方を纏めました

Angular でテストコードの書き方を纏めました

Angular でテストする方法についてまとめました。

jasmine(ジャスミン)というテストフレームワークが用意されています。Karma(カルマ)というテストランナーが標準で用意されています。Angular はJasmine + Karmaでテストコードを書けるように標準装備されているようです。

Angular では~spec.tsというファイルがデフォルトで用意され、このファイルがテストファイルになります。

テストスイートはdescribe,一つのテストはitで書きます。

テストを実行するには

または

で実行します。(ng test)

npm scriptsは、start,stop,testの場合のみrunを省略する事ができます。

このテストはスキップしたい、という場合はxdescribe,xitなどというように最初にxをつけます。

逆にこのテストのみ実行したい、という場合はfdescribe,fitというように最初にfをつけます。

skip,onlyのほうがわかりやすいですね、、。fはおそらくfocus,xはわかりません。

beforeEachメソッドとafterEachメソッド

各itのテストの前に実行されるbeforeEachメソッド、afterEachメソッドが用意されています。

Matcher の種類

Matcher は豊富に用意されています。基本的には以下のように記述します。

Matcher によっては以下のように書きます。例えばtoBeTruthy()のようなMatcher の場合です。

主なMatcher です。

Matcher 検証
toBe 同一オブジェクトかどうか
toEqual 同一値かどうか
toBeTruthy trueかどうか
toBeFalsy falseかどうか
toBeNull nullかどうか
toBeNaN NaNかどうか
toThrow 例外が発生するかどうか
toThrowError 例外が発生するかどうか
toContain 実効値が期待値に含まれているかどうか
toBeLessThan 実効値が期待値より大きいかどうか
toBeGreaterThan 実効値が期待値より大きいかどうか
toBeDefined undefinedではないこと
toBeUndefined undefinedであること
toHaveBeenCalled メソッドが実行されたこと
toHaveBeenCalledWith メソッドが実行されたこと(引数チェックも行える)

否定形も使えて、その場合はnot.MatcherとすればOKです。

サービスやパイプのテストは簡単ですが、コンポーネントのテストが大変です。

デフォルトで用意されているapp.component.spec.tsファイルを見るとよくわかると思います。

beforeEachメソッドでTestBed.configureTestingModuleメソッドでテストコンポーネントを定義します。

TestBed.createComponentメソッド

このメソッドでコンポーネントのインスタンスを作成します。

fixture.detectChangesメソッド

このメソッドが、コンポーネントの変更を検知する重要なメソッドになります。

@Injectableデコレータが付いている物に関してはbeforeEachでDIすることが可能です。

以下記述例です。

サービスをモックしたい場合にはスパイを使用する

コンポーネントによってはサービスに依存すると思います。こういった場合はスパイでモックし、コンポーネントとしての動作を担保します。サービスはサービスで動作を担保すればよいわけです。

サービスのメソッドをモック(スパイ)するには、spyOnを使います。

だいたい上記のような書き方になるかと思います。

サービスのメソッドをspyOnするということは非同期になりますのでasync/awaitを使いましょう。

スパイしたメソッドから意図的にthrowする

spyOnでスパイしたメソッドからthrowさせることができます。

これで例外を発生させることができます。

エラーでもカスタムエラークラスなどを作成していて、そのカスタムクラスを返したい場合はthrowErrorではなく、callFakeで返します。CustomErrorクラスとします。

関数を定義してカスタムクラスをthrowして返してあげればcatch句に入ります。

プライベートメソッドはspyOnできませんので、プライベートメソッドをthrowさせたい場合は別の方法で実現させる必要があります。

プライベートメソッドでthrowさせる方法

プライベートメソッドでthrowさせるには、以下のように関数を代入します。

ちなみにspyOnの戻り値はjasmine.Spyです。

テストコードからプライベート変数、プライベートメソッドにアクセスする

コンポーネントにprivate修飾子をつけていると、component.~~としてアクセスすることができません。以下の記述方法でアクセスする必要があります。

上記はprivateメソッドをモックしたようなイメージです。privateメソッドとprotectedメソッドのアクセス方法は同じです。そうではなく、privateメソッドを実行したい場合は以下のように()をつけます。

これでprivateメソッドを実行することができます。

さらにモックしたprivateメソッドを実行するには以下のように記述することで、実行が可能です。いずれも()を付ける必要があります。

privateメソッドをspyOnする方法

privateメソッドをspyOnすることができます。spyOnする場合にインスタンス名 as anyとすることによってpublicメソッドだけでなくprivateメソッドもspyすることができるようになります。以下、記述例です。

spyOnしてもメソッドの実際の実装は実行する

spyOnされたメソッドは実行はされないため、カバレッジがグリーンになりません。

これだとカバレッジ網羅率が上がらないため、spyOnしながら実際の実装を実行するようにすることができます、and.callThrough()メソッドを使用します。以下、記述例です。

これでメソッドが呼ばれたことも確認できますし、カバレッジもグリーンになります。

戻り値の型を確認する

jasmine.anyを使用することによって戻り値の型を確認することができます。

以下はPromiseオブジェクトであることを確認しています。

この結果は成功します。文字列の場合は、jasmine.any( String )とします。

オブジェクトの場合は、jasmine.any( Object )とします。

ブレークポイントを貼ってテストをデバッグする

karma.conf.jsやlaunch.jsonを編集すればできるみたいですが、まだ調査中です。

2018/07/11追記

色々調べていると、VSCodeでコード中に、debuggerというキーワードをタイプすればデバッグできるようです。

tslintをしているとエラーか警告になりますが、コメントで回避すればよいです。

テストコード(~.spec.ts)内に記述し、テストを実行するとVSCode上で止まってくれます。

正確には以下の手順です。

  1. ブレークしたい箇所にdebuggerとタイプする
  2. npm testを実行する
  3. デバッグを開始し、デバッグパネルを表示する

これでテストコードもデバッグすることができるようになりました。

ブレークポイントで止まってしまえば、そのあとは、F8やF5が使えます。ウォッチなども可能です。

日付はnew Date()しない方が良い

new Date()だと、テストする日によってテスト結果が変わる可能性があるので、日付を指定したほうが良いです。beforeEach()メソッド内か各it内に追加しておくと便利です。

カバレッジについては「Angular でカバレッジレポートを出力する」を参照ください。

privateインスタンスのprivateインスタンスのpublic変数をモックする

変数は配列とします。例えば空の配列でモックします。

このように記述することで階層が深くなっても関係なくモックすることが可能です。

JQuery<HTMLElement>型の変数を作成する

テストしたいイベントハンドラの引数がJQuery<HTMLElement>型の場合、無理やり作ってみました。

jQueryオブジェクトのonメソッドはtriggerメソッドで発火させる

上記のようなコードは、jQuery.trigger('change');でカバレッジを通す事が可能です。

onメソッドで第二引数にセレクタが指定してある場合は、triggerメソッドでカバレッジを通す事が出来ませんでした。誰か教えて下さい、、。

toHaveBeenCalled()でメソッドを呼ばれていることを確認するにはspyOnしておく必要がある

toHaveBeenCalled()でメソッドが呼ばれていることを確認しようとするとエラーが出てUsageが表示されます。

Usageを見るとspyObjでないとtoHaveBeenCalledは使えないようです。ということでテストの最初にspyOnしておきます。

Event.targetをダミーで作成する

Event.targetはreadonlyのため、セットすることができません。

ダミーで作成するのにちょっと工夫が必要です。

click()することによりaddEventListenerが実行され、その中の第二引数でevent.target.~~を設定することが可能です。正直かなりハマってしまいました。

実はeventと書くだけでokayみたい

Eventオブジェクトの生成にはかなりハマってしまったのですが、普通にeventと記述するだけでもokayでした。。windowと記述するのと同じですね。ただし、そのeventを使用して複雑なロジックを書いているようなら、上記のようにaddEventListener内に書いたほうがよいかもしれません。

ちなみにMouseEventの場合は、インタフェースを以下の通り変更するだけです。

というか同じ構造のオブジェクト作るだけでokay

色々試行錯誤していると、要するにEventに拘る事なく同じ構造のオブジェクトをつくってあげれば良いです。以下、例です。

moment型とmoment型をtoEqualで比較できない

Jasmineではどうもmoment型とmoment型は比較に失敗します。ポインタを比較しているからか?よくわかりませんが失敗するのでgetDate()メソッドを使って比較すれば期待する結果が得られます。

参考サイト

toThrowとtoThrowErrorはasyncファンクションでは使えない

プログラムでtry-catchしてthrow句のテストコードを書きたい時、throwされたらtoThrowで確認できると思ったらできたりできなかったりで挙動がどうも怪しいです。

色々調べてみると、asyncファンクションのイベントハンドラではtoThrowもtoThrowErrorも使えません。普通のファンクションなら使えるようです。

このようなコードを書いてテストコードを書いてみましたが、asyncがあるとないとでtoThrowが使えたり使えなかったりしますので注意です。

asyncファンクションの状態でテストコードを書いてみます。

これはエラーとなります。以下、参考サイトのようにthrowされた時のmessageで確認したほうが良いです。

参考サイト

コンストラクタ内で分岐があると大変

コンストラクタが実行されるのはnewされるときなので、コンストラクタ内の分岐をテストするのは難しいです。

サービスなどに依存している場合はinject内で行います。injectでは、@Injectable()しているクラスをDIすることができます。

[object ErrorEvent] thrown

このエラーが発生したら、HTML側のエラーだったことがあります。

例えば以下のような記述の場合、dataがundefinedの場合、エラーとなります。

これは以下のように書き換えた方が良いです。

これならdataがundefinedでもエラーにはなりません。

karma-parallelでテストを並列に実行し高速化する

karma-parallelプラグインでブラウザを複数起動し、テストすることが可能です。

karma.conf.jsを修正します。

executorsの個所は2としても良いですし、デフォルトは1です。上記は計算しています。

参考サイト

テストケースが多くなるとマシンスペックによってはテストで、DISCONECCTEDと表示されてしまうようです。karma.conf.jsの設定に以下を追記することによって回避することはできました。

KarmaとJasmineのバージョンを上げることによっても回避できるようです。

NgbTabChangeEventのダミーオブジェクトを作成する方法

イベントハンドラの引数がNgbTabChangeEventの場合のテストで困ったのでメモです。

NgbTabChangeEventはインターフェースでnewすることはできません。

activeIdとnextIdとpreventDefaultを持つオブジェクトであることが分かるので以下のようなモックイベントを作成します。

NgbPopoverのダミーオブジェクトを作成する方法

これもイベントハンドラの引数の型がNgbPopoverの場合にダミーを作成する必要がありましたのでメモです。以下ではcloseメソッドだけ定義しています。必要に応じて定義していけばよいです。

このイベントハンドラのテストのモック作成は以下のようにします。

constructorにChangeDetectorRefがあるコンポーネントをnewする方法

コンポーネントをnewするテストはまれだと思いますが機会があったのでメモです。

_elementRef.nativeElementをモックする方法

これも適したオブジェクトを作成してあげればモックすることが可能です。

Windowオブジェクトのモックは難しい

色々頑張ってみたけどネイティブオブジェクトをモックするって難しい。

出来たのはopenとcloseをspyOnするだけ。

window.locationをモックしようとすると、readonlyだからかどうがんばっても出来ません。

試したがNGだったのは以下。

いずれもモックできませんでした。色々調べてみると、このサイトを見つけました。

readonlyでも、writableやconfigurableによって再定義したりすることが可能なようです。

writableやconfigurableを調べるには

で調べることができます。

reloadの場合は以下になります。

writableもconfigurableもfalseなので、モックすることはできません。

window.cryptoのようにconfigurable:trueなら、再定義してモックすることが可能です。(奥が深い、、)

getアクセサはspyOnPropertyを使う

getアクセサはspyOnpropertyでモックすることができます。

上記はgetアクセサがbooleanを返す例です。

setアクセサは代入する

setアクセサは簡単で、代入するだけです。

これでget,setアクセサのモックが可能です。

各テストでexpectしないといけない

it単位でテストをしますが、expectをしていないテストは、「‘Spec ‘テストタイトル’ has no expectations.’」というエラーが表示されます。必ずexpectして期待値と実効値を比較しなければいけません。

jasmine.getEnv().allowRespy(true);

同じインスタンスの同じメソッドをspyOnするとします。その場合、以下エラーが出ます。

このため、一旦リセットしてあげる必要があります。それが

です。リセットしたい行でこの1文を書いてあげればよいです。

spyOnPropertyをリセットする

spyOnのリセット方法はjasmine.getEnv().allowRespy(true);とするだけですが、spyOnPropertyをリセットするにはこの1行ではできませんでした。

jasmine.Spyにあるcalls.reset()を使用する必要があります。

console.logを抑制する

プログラム上にconsole.logやconsole.errorを書いている場合、テストコードを実行するとコンソール上に出力されてしまいます。

これを抑制するにはspyOnを使用します。

alertを抑制する

alertはそもそもwindow.alert()なので、spyOnで以下のようにコーディングすることで抑制することができます。

ディレクティブのテストはダミーコンポーネント作成する

また後日、、。

動作環境です。

環境 バージョン
Karma 2.0.2
Jasmine 3.0

公式サイト

スポンサーリンク
  • このエントリーをはてなブックマークに追加
  • Evernoteに保存Evernoteに保存
スポンサーリンク

コメントをどうぞ

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA