宣言的に書けるテストライブラリを作りたい
React Testing Library で UI テストを書いていると、「 もっと宣言的に書きたい!」という願望が出てきた。しかし、私の狭い視野では宣言的に書ける感じのライブラリが見当たらなかったので、「 もう自分で作るか! 」となった。なので、このスクラップで、そのライブラリのアイディアを簡単にまとめる🖊
※ もしスクラップの内容と似たようなライブラリを知っている方が居ましたらコメントして頂けると嬉しいです🤗
設計方針
基本的には、宣言的に UI テストを書けるようにする方針。
具体的に言うと、要素の取得・判定やマウスイベント処理などを JSX( TSX ) で書けるようにする。ここで誤解されたくないのが、セマンティクスを JSX( TSX ) によって指定するのであって、テストコードを React などの描画ライブラリなどによって記述することではない。
なので、Vue や Angular、Svelte などのライブラリを使っていても、JSX( TSX ) を使用してテストコードを書く必要がある。
対応するライブラリ
React, Vue, Angular, Svelteには対応したい。
また上記でも少し触れたが、どの描画ライブラリを使っていても同じテストコード( JSX/TSX )になるように設計したい。
コンポーネントについて
UI における宣言的プログラミングには、コンポーネント指向による UI のコンポーネント化がある。これを有効活用できるように設計したい。
例えば、<Header />
内に <UserIcon />
がある場合は以下のように書けるようにしたい👇
const screen = render(<Header />) // <Header />を描画する
const exist = screen.has(<UserIcon />) // 要素の有無を検証する時にコンポーネントを使用できる
expect(exist).toBe(true) // 判定内容を検証する
使用できる関数を絞る
私の理想は、テスト内容が同じなら誰が書いても同じコードになること。
テストコードはドキュメントとしての役割もあるので、テスト内容が同じなのにコードが違っていては、ドキュメントとしての役割を果たすことが難しくなる。なので、誰が書いても同じコードになる事には大きな意義がある。
そのために、使用できる関数などを絞って実装したいし、jest の expect()
なども、なるべくは拡張せずに実装するべきだと思っている。
懸念点について
いくつかの懸念点について考察していこうと思う。
宣言的になるとテストコードが壊れやすくなるのでは?
JSX/TSXに依存関係を隠蔽すれば、多分壊れにくくなると思う。
例えば、描画ライブラリごとにラッパー関数を通すようにする👇
import { convertReact, convertVue, convertAngular } from "xxx"
// テストライブラリ用の JSX(TSX) で書けるように変換する
const Com1 = convertReact(ReactComponent)
const Com2 = convertVue(VueComponent)
const Com3 = convertAngular(AngularComponent)
上記のようにする事で、どのライブラリでも同じテストコードを使用できるようにする。こうすることで、例え Vue -> React に変更したとしてもテストコードは少ない変更で済む(はず) 。
テストコードが冗長になるのでは?
恐らく、最初の内は冗長になる事が予想される。しかし、コンポーネント化に対応したテスト設計を駆使することで対応可能だと思う。
パフォーマンスが落ちるのでは?
一応、コンポーネントに対してキャッシュをなどをして高速化することは可能かも?
JSX/TSX記法にどうやって対応するのか?
コンパイラを作ればいいじゃない( 脳筋 )
描画判定を宣言的に書ける
理想のソースコードを見てもらった方がいいと思うので、以下にそれを示す👇
※ Jestを使ってテストを書いている事を想定する
※ソースコードは暫定的なモノであり、開発する時には変更される可能性がある事に注意されたし。
import { render } from "xxx"; // 作ろうとしているライブラリ
import { SearchInput } from "./SearchInput"; // 検索フォームコンポーネント
describe("<SearchInput />のテスト", () => {
test("入力フォームを表示している", () => {
// コンポーネントを描画
const screen = render(<SearchInput />);
// <SearchInput /> が <input className="search-input" /> を持っているか判定する
const result = screen.has(<input className="search-input" />);
// 結果を検証する
expect(result).toBeTruthy();
});
});
上記では、<SearchInput />
が <input className="search-input" />
を持っているかを検証する。
マウスイベントも宣言的に書く
簡単な例を元に、イベント関連のテストコードをいくつか示す。
ソースコードは暫定的なモノであり、開発する時には変更される可能性がある事に注意されたし。
ホバー
例えば、ホバーなどでスタイルが変更されていることをテストしたい場合は以下のように書く👇
import { render } from "xxx"; // 作ろうとしているライブラリ
import { Button } from "./Button";
import { LoginForm } from "./LoginForm";
describe("<SearchInput />のテスト", async () => {
test(`ログインボタンにホバーした時 className が "hover" になっている`, () => {
// コンポーネントを描画
const screen = render(<LoginForm />);
// ログインボタンにホバーする
const resultScreen = await screen.hoverTo(<Button>ログイン</Button>);
// ログインボタンの className が "hover" になったか判定する
const result = resultScreen.has(<Button className="hover">ログイン</Button>);
// 結果を検証する
expect(result).toBeTruthy();
});
});
クリック
要素をクリックして、関数が実行されたかを検証したい場合は以下のようにする👇
import { render } from "xxx"; // 作ろうとしているライブラリ
import { Button } from "./Button";
import { LoginForm } from "./LoginForm";
describe("<LoginForm />のテスト", async () => {
test(`ログインボタンをクリックしたら Props で渡した関数を実行する`, () => {
// モック関数を作成して描画する
const onLoginMock = jest.fn();
const screen = render(<LoginForm onLogin={onLoginMock} />);
// <Button /> をクリックする
await screen.clickTo(<Button>ログイン</Button>);
// onLoginMockが実行されたか検証する
expect(onLoginMock).toHaveBeenCalled();
});
});
State関連の処理
State関連の処理を書きたい時は、以下のようにする👇
import { renderState, convertReactHooks } from "xxx"; // 作ろうとしているライブラリ
import { useLoginFormHook } from "./useLoginFormHook ";
// React Hooksをテストできる形に変換する
const State = convertReactHooks(useLoginFormHook);
describe("<LoginForm />のテスト", async () => {
test(`onLogin() を実行したら isLoading が true になる`, () => {
// Stateの情報を取り出す
const screen = renderState(State);
// stateを変更する関数を実行する
screen.state.onLogin();
// isLoading が true になったか判定する
expect(screen.state.isLoading).toBe(true);
});
});
とりあえず簡単な所はまとめたので、いったんクローズ。
また何か思いついたら、追記します🖊