(用意していた記事が壮大になってきて、本日中に書き終わらないと思ったので、急遽極小ネタに切り替えました。)
前日、同僚の@toshi__tomaがjscodeshiftについて投稿しているのですが、TDD的にAST変換を書けるのがjscodeshiftの強みの一つかなと思ったので、jscodeshiftのテストについて書きます。@toshi__tomaの記事↓と合わせて読んでいただけると、ちょうどよいかな〜と思います。
JavaScriptのリファクタリングツール「jscodeshift」の使い方 - Qiita
準備
jscodeshiftとjestをインストールします。
$ npm i -D jscodeshift jest
何も変換しないtransformer.js
を用意します。
// transformer.js
// 変換せず、そのままソースコードを返すだけ
module.exports = function (fileInfo) {
return fileInfo.source;
}
テストを書く
「書く」と言ってもとても簡単で、変換前のコードと変換後に期待されるコードを用意し、テストファイルでそれらを読み込む関数を実行するだけです。
テストデータの用意
まず、変換前後のコードを置く__testfixtures__
ディレクトリを作成します。
$ mkdir __testfixtures__
変換前後のコードを適当に作ります。変換前のコードは.input.js
、変換後に期待されるコードは.output.js
の拡張子を付けます。「何も変換しない」ので、まずは全く同じコードを置いてみましょう。
$ echo "console.log('hoge');" > __testfixtures__/hoge.input.js
$ cp __testfixtures__/hoge.input.js __testfixtures__/hoge.output.js
テストファイルの設置
テストファイルを__tests__/
以下に置きます。jestは、__tests__
以下にテストファイルを置く方法以外に、テストファイルの拡張子を.test.js
や.spec.js
にする方法もありますが、変換処理が記述されたJSファイル(今回の場合、transformer.js
)や__testfixtures__
が、テストファイルの一つ上の階層にあることを前提に処理するので、テストファイルは__tests__
以下に置きましょう。
$ mkdir __tests__
$ vim __tests__/transformer.js
テストケースの定義
jscodeshift/dist/testUtils
に定義されているdefineTest関数を使うと、テストケースを定義できます。defineTest関数の内部でjestのdescribe関数やit関数を使ってテストケースを追加してくれるので、テストファイルにはdefineTest関数以外に記述する必要はありません。
defineTest関数の引数は以下のとおりです。
- 第1引数: テストファイルのディレクトリパスなので常に
__dirname
を渡す - 第2引数: テスト対象の変換処理が記述されたJSファイル名(拡張子不要)
- 第3引数: 変換処理JSに渡すオプション。無ければ
null
- 第4引数: テストデータのプリフィックス。
__testfixtures__/第4引数.(input|output).js
がテストデータに利用される
// __tests__/transformer.js
const { defineTest } = require('jscodeshift/dist/testUtils');
// transformer.jsに対して、
// __testfixtures__/hoge.(input|output).jsを使ってテストする
defineTest(__dirname, 'transformer', null, 'hoge');
ちなみに、defineTest関数の第4引数を省略するとテストデータのプリフィックスとして第2引数が利用されます。
// 第4引数の省略
// transformer.jsに対して、
// __testfixtures__/transformer.(input|output).jsを使ってテストする
defineTest(__dirname, 'transformer');
では、テストを実行してみましょう。何も変換していないので、PASSします。
$ npx jest
PASS __tests__/transformer.js
transformer
✓ transforms correctly using "hoge" data (157ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.814s, estimated 1s
Ran all test suites.
console.log('hoge');
をconsole.log('fuga');
に変換するように修正してみましょう。__testfixtures__/hoge.output.js
をconsole.log('fuga');
に書き換え、テストを再実行します。ちゃんと、FAILしてますね。
# 期待する結果を変更
$ echo "console.log('fuga');" > __testfixtures__/hoge.output.js
# テストを再実行
$ npx jest
FAIL __tests__/transformer.js
transformer
✕ transforms correctly using "hoge" data (54ms)
● transformer › transforms correctly using "hoge" data
expect(received).toEqual(expected) // deep equality
Expected: "console.log('fuga');"
Received: "console.log('hoge');"
at runInlineTest (node_modules/jscodeshift/dist/testUtils.js:36:33)
at runTest (node_modules/jscodeshift/dist/testUtils.js:73:3)
at Object.<anonymous> (node_modules/jscodeshift/dist/testUtils.js:90:7)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 0.712s, estimated 1s
Ran all test suites.
テストが通るようにtransformer.js
を修正します。
// transformer.js
module.exports = (fileInfo, { jscodeshift: j }) => {
const root = j(fileInfo.source);
root.find(j.Literal, { value: "hoge" }).forEach(path => {
path.replace(j.literal("fuga"));
});
return root.toSource({ quote: "single" });
};
テストを再実行すると、PASSします!
$ npx jest
PASS __tests__/transformer.js
transformer
✓ transforms correctly using "hoge" data (113ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.276s
Ran all test suites.
このように、.output.js
を修正→テストが通るまで実装を繰り返すことで、進捗を確認しながら着実に進めることができます。
defineTest関数以外にも、jscodeshift/dist/testUtils
には以下のような関数が定義されています。お好みで使い分けましょう。
- defineInlineTest関数
- テストデータを直接文字列で渡す版
- defineSnapshotTest関数
- jestのtoMatchSnapshotを使って、回帰テストを書くことができる
- applyTransform関数
- 変換を実行する関数。入力を引数に渡し、出力を返り値で受け取る
おわり
jscodeshiftのテストの書き方を紹介しました。jscodeshiftは、ASTの入門としておすすめですので、興味のある方はぜひ触ってみましょう〜