atom-mocha-test-runnnerの使い方（実践編）

[Atomアドベントカレンダー2017](https://qiita.com/advent-calendar/2017/my-calendar)の10日目の記事です。

今回は[前回](https://qiita.com/Kesin11/items/f45ef70bda95a4f20418)紹介した[atom-mocha-test-runner](https://github.com/BinaryMuse/atom-mocha-test-runner)の詳しい使い方を紹介します。

## atom-mocha-test-runnerをテストランナーに設定する
まずは前回紹介したようにatom-mocha-test-runnerでテストが走るようにセットアップしましょう。

- atom-mocha-test-runner, chaiをインストール
- pakcage.jsonに"atomTestRunner": "atom-mocha-test-runner"を追加
- spec/にある**spec.jsを**.test.jsにリネーム

ここまでできたら`apm test`を実行しましょう。

```Error: Invalid Chai property: toBe. Did you mean "to"?```

といったAtomが元々用意してくれていたアサーションを使えないエラーが出ればOKです。
**実行しても何も表示されなかったり、テストがpassしてしまった場合は正しく設定できていないので再度確認してみてください**

## specをmochaとchaiで動くように書き直す
テストがmochaで走るようになったことで元々Atomが用意していたwaitsForPromise, runsといった非同期テストのための構文や、toBe, toHaveLengthといったアサーションが使えなくなっています。
これらを書き換えてテストが動く動くようにしていきましょう。ちなみにここではアサーションライブラリとしてchaiを使いますが、atom-mocha-test-runnerはchaiに依存しているわけではないので好みのアサーションライブラリに差し替えが可能です。

### 非同期処理のテスト
まずは非同期処理のためにかつて使われていたwaitsForPromise, runsとおさらばしましょう。

今のAtomとmochaはasync/awaitをサポートしているので、このように書き直すことができます。

```my-atom-package-template-spec.js
// 昔ながらのwaitsForPromise, runcを使う場合
// 説明に必要なところだけ抜粋
beforeEach(() => {
  activationPromise = atom.packages.activatePackage('my-atom-package-template');
});

describe('...', () => {
  it('...', () => {
    atom.commands.dispatch(workspaceElement, 'my-atom-package-template:toggle');

	// Promiseがresolveされるまで待つ
    waitsForPromise(() => {
      return activationPromise;
    });

    // waitsForPromiseが完了したら実行される
    runs(() => {
      expect(...)
    });
  });
});
```

```my-atom-package-template.test.js
// async/awaitで書き直した場合
// 説明に必要なところだけ抜粋
beforeEach(() => {
  activationPromise = atom.packages.activatePackage('my-atom-package-template');
});

describe('...', () => {
  // itにasyncを付ける
  it('...', async () => {
    atom.commands.dispatch(workspaceElement, 'my-atom-package-template:toggle');

	// Promiseがresolveされるまで待つ
	await activationPromise

	expect(...)
  });
});
```

今時のjsのテストフレームワークと同じようにasync/awaitが使えることでだいぶスッキリしましたね！

ちなみにmochaはasync/awaitを使う方法以外の非同期処理を扱う構文も存在しているので、一度mochaの[ドキュメント]()を見てみると良いでしょう。

### アサーションの書き換え
次はexpect, toBeといったアサーションの書き換えです。

expectについてはchaiをimportするだけで書き換える必要はないです。

問題はtoBe(true)やtoHaveLength(0)などのアサーションで、chaiでも同等の機能が存在するのですが名前が変わっています。
例えば上の2つはchaiで書き直すとこのようになります。

```chai.test.js
expect(...).to.be.true
expect(...).to.have.lengthOf(0)
```

数が多いと大変ですが[chaiのドキュメント]()を見ながら書き換えていきましょう。自分の場合は正規表現でまとめて置換して対応しました。

ちなみに、chaiは`expect`以外にも`assert`や`should`を使うことができます。今回は`expect`を例にしましたが好みに応じて変えてもいいでしょう。

さて、これでテストが動くようになったはずです。試してみましょう！
おや、```Error: Failed to load package 'my-atom-package-template'```で動かない？
ようこそ、ここからが実践編です :innocent:

## パッケージがロードできない問題を解決する
さて、エラーの原因を見ると`atom.pakcages.activatePackage('...')`したときにそのパッケージを読み込めないことが原因らしいです。
この理由は、atom-mocha-test-runnerがデフォルトの設定だと[global.atomにセットするatomのオブジェクトを作るときconfigDirPathにテスト用の空のディレクトリをセットしている](https://github.com/BinaryMuse/atom-mocha-test-runner/blob/v1.2.0/lib/create-runner.js#L40)からのようです。

どうやらbuildAtomEnvironment()というのを使えばatomのオブジェクトを作れることがコードから推測できます。
試しにgithubでatomのソースコードからbuildAtomEnvironmentを検索するとjasmine-test-runner.coffeeというファイルが見つかりました。おそらくこれがAtomに元々組み込まれているテストランナーでしょう。
その中をさらに検索してみると[window.atomにセットしているそれらしい処理](https://github.com/atom/atom/blob/fc53b2185716174dc700a963e8fb73e68a921449/spec/jasmine-test-runner.coffee#L24-L28)があります。

configDirPathにprocess.env.ATOM_HOMEというのを渡しているので、昔のAtomのテストランナーではおそらく~/.atomのパスを渡すことで自分のAtomの環境を使ってテストを走らせていたのだろうということが推測できます。


原因と対策がわかったので先程のテストの中に組み込みましょう。
beforeEachの中でこのようにwindow.atomに代入します。

```js
window.atom = global.buildAtomEnvironment({
  window: window,
  configDirPath: process.env.ATOM_HOME,
  enablePersistence: false
})
```

これでめでたくテストが通ったはずです！


## CIに乗せる
めでたくテストが通ったので[atom/ci](https://github.com/atom/ci)から.travis.ymlあたりをコピーしてCIサービスでテストが走るようにしてみましょう。

はい、残念ながらCIではテストが失敗します :innocent:

なぜか？その理由は、CI上の環境では今テストしようとしているパッケージがAtomにインストールされていないからのようです。
インストールされていないので`configDirPath: process.env.ATOM_HOME`の設定をしたところで当然ロードできるわけがない、と。
atom-mocha-test-runnerを使う前は問題なかったのにこれが問題になる理由は不明です。色々調べたものの、残念ながら自分も原因を突き止めることはできませんでした・・・。

この問題のワークアラウンドとして、[atom/welcomeが行っている](https://github.com/atom/welcome/blob/v0.36.5/test/welcome.test.js#L23)ように`atom.packages.activatePackage()`は使わずにテスト対象のパッケージをimportして直接activate()を呼び出して起動することで回避できます。

PackageGeneratorで作られるテストの雛形をこのように変更します。

```my-atom-package-template.test.js
// 必要な箇所だけ抜粋
import myAtomPackageTemplate from '../lib/my-atom-package-template';

describe('', () => {
  it('', async () => {
    // activate()をawaitで待たないと次のdispatch()が失敗することに注意
    await myAtomPackageTemplate.activate({});
    atom.commands.dispatch(workspaceElement, 'my-atom-package-template:toggle');
  });
)};
```

PackageGeneratorによって生成されるパッケージの雛形に全てここまで紹介した内容を全て適用したコードはこちらになります。
https://github.com/Kesin11/MyAtomPackageTemplate/tree/v1

## 終わりに
お疲れ様でした。以上で昔の古くてテストの書き方を脱却し、めでたくモダンなテストの書き方ができるようになりました :tada:

なかなか長い道のりですがAtom本体のAPIは最近どんどん非同期対応が進んでいるため、テストの方も非同期を扱いやすくしておかないとwaitsForPromiseを至る所で使うハメになりテストが読みづらくなってしまいます。

既にパッケージをメンテしている方も、これから新しいパッケージを作る方もatom-mocha-test-runnerを使ってテストを書いてパッケージをメンテしやすくしていきましょう :smile:

## 番外編 runner.jsを用意する
ここまででatom-mocha-test-runnerを使ったテストの実行は完了ですが、テスト周りの細かい設定を変更したい場合はpackage.jsonの`atomTestRunner: "atom-mocha-test-runner"`を`atomTestRunner: "spec/runner.js"`に変更し[^1]、runner.jsでcreateRunner()にオプションを渡すことでデフォルトの挙動から変更できます。

例えば`testSuffixes: ['-spec.js', '-spec.coffee']`というオプションを設定すれば従来通り**-spec.js, **-spec.coffeeというファイル名がテストファイルとして見なされるように変更可能です。
どのようなオプションが設定できるかは[atom-mocha-test-runner](https://github.com/BinaryMuse/atom-mocha-test-runner)のREADMEに記載されています。

他にも例えば[atom/github](https://github.com/atom/github/blob/master/test/runner.js)を見ると`global.assert = chai.assert`として各テストの中でimportを省略できるようにしたり、mochaの設定を変えていたりするようです。

[^1]: 一般的にはtest/runner.jsに置くようですが特に制約は存在しないのでお好みのパスに変更可能です。