jscodeshiftのテストを書く

（用意していた記事が壮大になってきて、本日中に書き終わらないと思ったので、急遽極小ネタに切り替えました。）

前日、同僚の[@toshi__toma](https://twitter.com/toshi__toma)がjscodeshiftについて投稿しているのですが、TDD的にAST変換を書けるのがjscodeshiftの強みの一つかなと思ったので、jscodeshiftのテストについて書きます。[@toshi__toma](https://twitter.com/toshi__toma)の記事↓と合わせて読んでいただけると、ちょうどよいかな〜と思います。

[JavaScriptのリファクタリングツール「jscodeshift」の使い方 - Qiita](https://qiita.com/toshi-toma/items/93f1dfdf0a820fe6fccc#transform-file)

## 準備

jscodeshiftとjestをインストールします。

```console
$ npm i -D jscodeshift jest
```

何も変換しない`transformer.js`を用意します。

```js
// transformer.js
// 変換せず、そのままソースコードを返すだけ
module.exports = function (fileInfo) {
  return fileInfo.source;
}
```


## テストを書く

「書く」と言ってもとても簡単で、変換前のコードと変換後に期待されるコードを用意し、テストファイルでそれらを読み込む関数を実行するだけです。

### テストデータの用意

まず、変換前後のコードを置く`__testfixtures__`ディレクトリを作成します。

```console
$ mkdir __testfixtures__
```

変換前後のコードを適当に作ります。変換前のコードは`.input.js`、変換後に期待されるコードは`.output.js`の拡張子を付けます。「何も変換しない」ので、まずは全く同じコードを置いてみましょう。

```console
$ 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__`以下に置きましょう。

```console
$ 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`がテストデータに利用される

```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引数が利用されます。

```js
// 第4引数の省略
// transformer.jsに対して、
// __testfixtures__/transformer.(input|output).jsを使ってテストする
defineTest(__dirname, 'transformer');
```

では、テストを実行してみましょう。何も変換していないので、PASSします。

```console
$ 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してますね。

```console
# 期待する結果を変更
$ 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`を修正します。

```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します！

```console
$ 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の入門としておすすめですので、興味のある方はぜひ触ってみましょう〜