株式会社HAMWORKS
😎

【WordPress】カスタムブロックのユニットテストに挑戦してみた

に公開

ユニットテストを書く練習を続けているのですが、ふと「カスタムブロックのユニットテストってどうやって書くのかな?」という疑問が湧いてきました。
コアブロックにはがっつりテスト書いてあるし、やりようはあるんだろうと調べることにしました。

https://developer.wordpress.org/block-editor/contributors/code/testing-overview/

このページによると、JavaScriptのテストは Jest を使って書くのが標準のよう。また、必要に応じて React Testing Library も併用できるとのこと。
普段からJestは使っているので、これなら取っつきやすいかも!と思い、さっそく試してみることにしました。

練習用環境の準備

まずは npx @wordpress/create-block@latest コマンドで基本的なブロックを作成。
TypeScriptで書きたかったので、生成されたJSファイルをTSに書き換えていきました。開発環境としては Studio by WordPress を使っています。

https://developer.wordpress.com/studio/

TypeScript対応のために必要なパッケージをインストール:

npm i --save-dev @types/wordpress__block-editor @types/wordpress__blocks
npm i --save-dev @types/jest ts-jest

jest.config.json も以下のように書き換えてTypeScript対応しました:

jest.config.json
{
  "preset": "@wordpress/jest-preset-default",
  "transform": {
    "^.+\\.(ts|tsx)$": "ts-jest"
  },
  "moduleFileExtensions": ["js", "ts", "tsx", "json", "node"],
  "testEnvironment": "jsdom",
  "testMatch": ["**/test/**/*.test.(ts|tsx|js)"]
}

テストしやすいブロックの設計について考える

さて、じゃあどこから調べていこうかな…と思っていると、社内SlackでToro_Unitさんから情報が。

カスタムブロックのテストですが、カスタムブロックそのものの unit test は結構難しいので、

  • コンポーネントに切り分けて単体テスト
  • カスタムブロックそのもののテストは playwright での e2e でやるってケース多いです

「カスタムブロック」って単位だと WordPress 上でレンダリングとかその辺の話のせいで unit test がクソ難しいので、
flexible table block とかだと、保存されてる table の markup <-> 内部的な table のデータ みたいなことをやってるけど、その部分は unit test
https://github.com/t-hamano/flexible-table-block/blob/main/src/utils/test/table-state.test.ts
カスタムブロック上でそれをテストするのが e2e って感じで分けてる

setAttribute とか useSelect とかがmock 出来ないから、Edit とか Save のコンポーネントそのものを単体テストが出来ないんですよね。(単体テスト = unit test)

とのこと。じゃあどういうテスト書いたらいいかな〜?ってChatGPTに相談すると、

  1. 属性入力のUIを独立したコンポーネントとして分離する
  2. データ変換処理はユーティリティ関数として切り出す
  3. ブロック全体の振る舞いは Playwright などでE2Eテストでカバーする

という方針が良さそうとのことだったので、今回はユーティリティ関数のテストに絞ることにしました。

実際に書いてみたテスト

今回試してみたのは、type という属性の値に応じてクラス名を出し分けるユーティリティ関数のテスト。

getClassNameByType.ts
interface GetClassNameByTypeProps {
	type?: 'warning' | 'info' | 'success' | 'error';
}

export default function getClassNameByType( {
	type,
}: GetClassNameByTypeProps ) {
	switch ( type ) {
		case 'warning':
			return 'message--warning';
		case 'info':
			return 'message--info';
		case 'success':
			return 'message--success';
		case 'error':
			return 'message--error';
		default:
			return 'message--neutral';
	}
}

これに対するテストコードはこんな感じで書きました:

getClassNameByType.test.ts
import getClassNameByType from '../getClassNameByType';

describe( 'getClassNameByType', () => {
	it.each( [
		[ 'warning', 'message--warning' ],
		[ 'info', 'message--info' ],
		[ 'success', 'message--success' ],
		[ 'error', 'message--error' ],
		[ undefined, 'message--neutral' ],
	] )( 'should return the correct class name for %s', ( type, expected ) => {
		expect(
			getClassNameByType( {
				type: type as
					| 'warning'
					| 'info'
					| 'success'
					| 'error'
					| undefined,
			} )
		).toBe( expected );
	} );
} );

超単純簡単なテストですが、テストが通ったときの達成感はやはり良いですね〜。

感想と今後の方針

「そもそもカスタムブロックのユニットテストってどうやるの?」という軽い気持ちで始めましたが、テスト容易性を念頭に置いた設計の重要性をよく考えないとな、という気づきを得ました。

今後カスタムブロックを作る際には、最初から次のようなアプローチを心がけていきたいと思います:

  • UI部分は小さく再利用可能なコンポーネントに切り出す
  • ロジック部分はユーティリティ関数として分離する
  • データの変換や条件分岐は純粋関数として実装する
  • ブロック全体の動作確認はE2Eテストで行う(E2Eテストはまだ書けないので今後の私に期待!!)

これらの方針を意識して、今後ブロックつくるときは一緒にテストも書いていこうと思います。

株式会社HAMWORKS
株式会社HAMWORKS

Discussion