こんにちは。みなさん、テストは書いてますか？

「Haskellライブラリ所感2016」という記事でも紹介されているとおり、Haskellにも様々なテスト用ライブラリーがあります。

今回は、「Haskellライブラリ所感2016」でも紹介されているsilentlyというパッケージにインスパイアされた、新しいテスト用ライブラリーを作りました。

タイトルにも書きましたがmain-testerといいます。

Link to

here main-tester ができること

main-testerは名前の通り、 main 関数のテストをサポートするライブラリーです。

Haskell製のプログラムを起動すると最初に実行される、あの main 関数です。

main 関数は IO () という型であるとおり、原則として必ず入出力を伴うので、自動テストがしにくい関数です。

一般的なベストプラクティスとしては、できるだけ IO でない、純粋な関数を中心にテストを書いていくのが普通でしょう。

それでも敢えて main 関数の自動テストを書くのには、以下のメリットがあります。

main 関数をテストすると言うことは、作っているコマンドの、ユーザーの要求に最も近いレベルのテスト、 E2E テスト（ end-to-end テスト）をすることができる。 main 関数（や、その他の IO を伴う関数）に対するテストは、データベースやファイルシステムなど、外部のソフトウェアとの「組み合わせ」で起こるバグを検出できる。 経験上、特に単純なアプリケーションでは、そうした外部のソフトウェアに対する「誤解」が原因となったバグが比較的多いように感じています。 私の個人的な都合ですが、趣味では小さなアプリケーションを書くことが多いので、そうした E2E テストの方が効果的だったりする。

このように、 main 関数をはじめとする、 IO な関数に対して敢えて自動テストを書くことには、様々なメリットがあります。

main-tester はそうした IO な関数をテストする際に伴う、2つの問題を解決しました。

標準出力・標準エラー出力に出力した文字列がテストしにくい ➡️ captureProcessResult という関数で、標準出力・標準エラー出力に出力した文字列をそれぞれ ByteString として取得することができます。 標準入力から文字列を読み出そうとすると、テストの実行が停止してしまう。 ➡️ withStdin という関数で、標準入力に与えたい文字列を ByteString として与えることができます。

ここに書いたことは、ビルドした実行ファイルを子プロセスとして呼び出すことによってもできます。

入出力の順番など、標準出力や標準エラー出力のより細かい挙動をテストするにはその方がいいでしょう 。

しかし、テストのために PATH を分離させる必要があったり、そのために stack exec を使ったらめっちゃ遅いという問題があったり、そもそも子プロセス呼び出しはそれだけでオーバーヘッドがあったりと、様々な問題があります。

物事をよりシンプルにするには、 main 関数を直接呼び出した方がよいでしょう。

main-testerは、CLIアプリケーションのE2Eテストにおける、そうした子プロセスの呼び出しの問題と、より大きな関数をテストしたいというニーズに応えるためのライブラリーなのです。

「silentlyというパッケージにインスパイアされた」と冒頭で申しましたとおり、前節で紹介した機能は、実はすでにほかのライブラリーに似たものがあります。

silentlyに加え、imperative-edslというパッケージに含まれる、 System.IO.Fake というモジュールです （ほかにもあったらすみません！🙇🙇🙇） 。

これらとmain-testerとの違いは何でしょう？

第一に、先ほども触れましたが、main-testerの captureProcessResult 関数や withStdin 関数は、標準出力・標準エラー出力・標準入力でやりとりする文字列をstrictな ByteString でやりとりします。

silentlyや System.IO.Fake は、 String なのです。

ByteString は文字通り任意のバイト列を扱うことができるので、「Unicodeの文字のリスト」である String よりも、多様なデータを扱うことができます。

これは、特に複数の種類の文字コードを扱うとき、非常に重要な機能となります。

以前の記事で取り上げた、 Invalid character というエラーを再現させる場合も、ないと大変やりづらいでしょう。

第二に、main-testerの captureProcessResult 関数は、 main 関数の終了コードも ExitCode 型の値として取得できます。

main 関数の中で exitFailure 等の関数を呼び出すと、 ExitCode が例外として投げられます。

既存のライブラリーでこれを行うと、 ExitCode が例外として処理されるため、テストしたい main 関数の実行が終了してしまいます。

結果、 main 関数が標準出力・標準エラー出力に書き込んだ文字列を取得することができないのです。

「○○というエラーメッセージを出力して異常終了する」といったことをテストしたい場合、これでは使いづらいでしょう。

「 main 関数のE2Eテストを行うためのライブラリーである」という観点から、必須の機能であると判断し、実装しました。 ちなみに、 ExitCode 以外の例外についてはそのまま投げられます。仕様を単純にするために、これはユーザーのテストコードの中で処理することとしています。

機能は非常にシンプルなので、使い方についてはドキュメントのサンプルコードを読めば大体わかるかなぁと思いますが、簡単にサンプルを載せておきましょう。

例えばこんなソース👇のプログラムがあった場合、

ExampleMain.hs:

main-testerを使えば、次のようにHspecでテストできます。

ExampleSpec.hs:

それぞれのファイルを同じディレクトリーに置いた上で、次のように実行すれば試せるはずです （cabalユーザーの皆さんは適当に読み替えてください…） 。