2024/12/16 Angular 日々のテストテクニック、1
https://scrapbox.io/files/67603a3f30b7e4ec0caa99b8.png
所属でフロントエンドをさわる場合には息の長いアプリケーションものによるんですが、Angular がほとんどです
一部 CoffeeScript + jQuery を使ったものもあります
一部 React + mobx を使ったものもあります
Angular を書く場合は Angular Testing Library (以降ATLと略す) を使う場面がほとんです
2年ほど前に Testing Library を React テスト応用、テストに悩む人へ で取り扱ったのですが、Testing Library を使うと UI を操作するようにテストが書けることがあり、フロントエンドのテストを書くのに最適であると思っています 提供される API は Angular でも変わらず、render での結果を検証したり、render された画面から Role を特定して取得した要素を操作し結果を検証したりするという点は何も変わりはありません
今日は Angular の UI テストを書いているときに知ったことです。端的に言えば以下!:
「ngModel は Change Detection で非同期に値を更新するので、テストを書く場合には注意したい」
refs:
ATL を書いているとどうしても UI をなぞるようにテストを書きたいです
ただ残念ながらそういうわけにはいかない場合も多いです
Template-driven formで実装する場合には素朴にテンプレートへ 以下のように書くこともあるでしょう
code:my-form-component.html
<label for="email">Your email</label>
<input type="email" id="email" name="email" (ngModel)="email" required email> <button type="submit" disabled="myForm.invalid">送信</button> </form>
Emailの入力は以下の必要があります
必須項目であるため空白文字ではない入力がある
Angular が提供する email バリデータディレクティブによる検証がとおる
上記を満たしてこのForm は Valid となります
UI 操作だけを考えて書いてみましょう
code:my-form.component.spect.ts
it('when inputs are fullfilled, button is able to submit.', async () => {
await render(MyFormComponent);
const email = screen.getByLabelText<HTMLInputElement>('Your email');
const btn = screen.queryByRole('button');
await userEvent.type(email, 'foo@bar.exmaple');
expect(btn).toBeEnabled();
});
実際これでとおりますし、UI をなぞるように書けていてバッチリなように見えますよね
ところがいつかこのテストはとおらなくなるかもしれません
理由は前述のとおりで ngModel の変更検知が非同期なのでいつ更新されるかわからないからです
CI で並行実行している場面でとおったり落ちたりするということが発生しないともかぎらない
CI ではなくても最近さわっていたリポジトリでは2000くらいのテストケースにおいて、ローカルでのテスト並行実行中に前述の書きっぷりで予期せずいくつかテストが落ちました
waitFor(() => /* 期待値検証 */) の書き方で濁せることもありますが、timeout がデフォルトで指定されていることと、その timeout のチューニングが必要になるということはすでに負けてしまっています
このままだと非常にFlakyなテストになりえるので以下のように書くようにしました
code:retake.spec.ts
it('when inputs are fullfilled, button is able to submit.', fakeAsync(async () => {
const { detectChanges } = await render(MyFormComponent);
const email = screen.getByLabelText<HTMLInputElement>('Your email');
const btn = screen.queryByRole('button');
await userEvent.type(email, 'foo@bar.exmaple');
detectChanges(); // 入力による意図的な変更検知
tick(); // ngModel の非同期の変更を検知を想定し MicroTask をハケさせる
expect(btn).toBeEnabled();
}));
refs:
今日は以上です