2017年9月5日
Bashアプリケーションをテストする

本記事は、原著者の許諾のもとに翻訳・掲載しております。

以前、bashスクリプトをテストする仕事に取り組んだことがあります。最初、Pythonユニットテストを使うことにしましたが、プロジェクトに外部技術を持ち込むのは気が進みませんでした。そこで、仕方なく、悪名高い     bash    で書かれたテスト用フレームワークを使いました。
既存ソリューションの概要
手に入るソリューションを探してGoogle検索しましたが、選択肢はほんの少ししかありませんでした。そのうちいくつかについて、詳しく見ていきましょう。
重要になるのは、どんな基準でしょうか?
- 依存関係: 
bassのテスト用フレームワークを選ぶときに、python、luaなどのシステムパッケージも一緒に引きずり込むのは嫌ですね。 - インストールの難しさ:継続的な開発の実装とTravis CIでの継続的な統合も仕事の1つだったので、私にとってインストールにかかる時間と手間数が妥当だということは、重要でした。理想的な選択肢はパッケージマネージャで、許容できる選択肢は 
git clone、wgetです。 - ドキュメンテーションとサポート:アプリケーションは複数の異なるUnixディストリビューションで実行できなければならないので、テストは、さまざまなプラットフォーム、シェル、それらの組み合わせを含めてどこででも、アップデートの速さに合わせて正常に働かなければならず、しかも、コミュニティやその他のユーザの経験から離れてのテストは望ましくありません。
 - 何らかの形でのフィクスチャと、(少なくとも!) 
setup()関数とteardown()関数が利用できること。 - 新しいテストを記述するための妥当な構文: 
bashの世界では重要な必要条件です。 - 慣習的な結果:テストを行った回数、何が成功して何が成功しなかったか、(必須ではないが、望ましくは)何が起こったか。
 
assert.sh
最初に目にとまった選択肢は     assert.sh    という小さなフレームワークでした。なかなか良いソリューションで、インストールと使い方が簡単です。最初のテストを記述するためには、     test.sh    というファイルを作成する必要があります(下の例はドキュメンテーションから引用しました)。
. assert.sh
# `echo test` is expected to write "test" on stdout
assert "echo test" "test"
# `seq 3` is expected to print "1", "2" and "3" on different lines
assert "seq 3" "1\n2\n3"
# exit code of `true` is expected to be 0
assert_raises "true"
# exit code of `false` is expected to be 1
assert_raises "false" 1
# end of test suite
assert_end examplesファイルを記述したら、それを実行できます。
$ ./tests.sh
all 4 examples tests passed in 0.014s.その他には、次のような利点があります。
- シンプルな構文と使い方
 - 優良なドキュメンテーションと使用例
 - 条件付きと条件無しのテストスキップが可能
 - フェイルファスト(fail-fast)や全実行(run-all)が可能
 - (フラグ-vを使えば)エラーの詳細を表示可能。初期設定では、どのテストが失敗しているか通知されません。
 
しかし、重大な欠点もいくつかあります。
- この記事の執筆時点では、Githubに「ビルド失敗」という赤いアイコンがあり、恐怖を感じました。
 - このフレームワークは簡単な部類に入るように見えましたが、各テストのためにデータを準備して完了時に削除するための 
setup()とteardown()のメソッドが欠如しています。 - ある1つのフォルダから全てのテストを実行させることはできません。
 
結論: 基本的な     shell    スクリプト用のシンプルなテストを書く必要があるなら、お勧めできる良いツールです。複雑な仕事には向いていません。
shunit2
    shunit2    のインストールは     assert.h    ほど簡単ではありません。私は適切なリポジトリを見つけることができませんでした。Google Codeにいくつか、Githubにも少数のプロジェクトがあり、3年から5年前にいろいろな段階で放置されています。さらに、     svn    リポジトリもいくつかあります。したがって、どのリリースが最新で、どうやってダウンロードするのかを知ることは不可能です。でも、それは大した不便ではありません。
テストはどのようなものでしょうか? 次の例は、ドキュメンテーションからの引用です。
testAdding()
{
  result=`expr 1 + 2`
  assertEquals \
      "the result of '${result}' was wrong" \
      3 "${result}"
}これを実行させます。
/bin/bash math_test.sh testAdding  
Ran 1 test.  
OKこのフレームワークは、このクラスなりのいくつかの独自機能を誇ります。
- コード内にテストスイートを作成可能です。この機能は、ある種のプラットフォームまたはシェルのためのテストがあるときには便利です。この場合、 
zsh_やdebian_などの自分専用の名前空間を使うことができます。 - 各テストについて実行できる 
setUp関数とteardown関数、テストセッションの最初と最後に実行できるoneTimeSetUpとoneTimeTearDownがあります。 - さまざまなアサートを幅広く選択でき、 
${_ASSERT_EQUALS_}を使ってテストが失敗したラインの数を入力することが可能。ただし、ラインのナンバリングがサポートされているシェル(bash(>=3.0)、ksh、pdksh、zsh)でのみ有効です。 - テストをスキップすることができます。
 
それでもなお、下記のような非常に不利な点がいくつかあったことで、結局は選択肢から消えました。
- プロジェクトがほとんど放置されています。
 - 上記の点から、何をインストールすべきかの判断が困難。直近のリリースは2011年のようです。
 - 機能の数が過剰気味。例えば、等式の確認に、 
assertEqualsとassertSameの2つの方法が存在します。信じられません。 - フォルダから全ファイルを実行することができません。
 
結論: 本格的なツール。十分柔軟にセットアップでき、プロジェクトに不可欠なパーツになり得ます。しかし、     shunit2    プロジェクトそのものが構造を欠いているのは怖いので、もう少し探してみることにしました。
roundup
最初にこのフレームワークに惹かれたのは、     ruby    の     Sinatra    の作者が書いたものだったからです。また、誰もが知っている     mocha    に似たテスト構文も気に入りました。ファイル内で     it_    から始まる関数は全てテストとみなされ、デフォルトで実行されます。面白いことに、全テストがそれぞれ独自のサンドボックス内で走るので、余分なエラーを避けられるのです。下記は、ドキュメンテーションにある事例です。
describe "roundup(5)"
before() {
    foo="bar"
}
after() {
    rm -f foo.txt
}
it_runs_before() {
    test "$foo" "=" "bar"
}出力の事例は紹介されていません。自分でインストールして確認しなければならず、実際それほど良いものではありません。しかし、プラスの面もあります。
- 各テストがそれぞれのサンドボックス内で実行されるのは、非常に便利です。
 - 使い方が簡単です。
 git cloneと./configure && make経由でインストール可能。加えてローカルディレクトリにインストールできるので、$PATHを編集するだけで済みます。
とはいえ、かなりの欠点もあります。
- 全テストに対する共通関数のソースを作成できません(本当に正確に言えば、不正をすれば可能)。
 - フォルダから全ファイルを実行することができません。
 - ドキュメンテーションは 
TODOマークだらけ、ここ数年開発が進んでいません。 - テストをスキップできません。
 
結論: 全くの平凡なツールで、良いとは言えずとも、それほど悪くもないといったところです。その機能はassert.shをより幅広くした感じです。どのような時に使えるでしょうか。     assert.sh    を使ってみて、後は     before()    や     after()    さえあれば、と思う場合には向いているかもしれません。
bats
言ってしまいましょう、最終的に私はこのフレームワークを選びました。気に入った点はたくさんあります。何より、優れたドキュメンテーションです。使用事例やセマンティックバージョン管理。そして、batsを使っているプロジェクトのリストを特に挙げたいと思います。
    bats    は、「内部の全てのコマンドがコード     0    を返すなら、テストは完了したとみなす(     set –e    がするように)」というアプローチを取っています。以下がbatsに書かれたテストの例です。
#!/usr/bin/env bats
@test "addition using bc" {
  result="$(echo 2+2 | bc)"
  [ "$result" -eq 4 ]
}
@test "addition using dc" {
  result="$(echo 2 2+p | dc)"
  [ "$result" -eq 4 ]
}そして出力は次のとおりです。
$ bats addition.bats
 ✓ addition using bc
 ✓ addition using dc
2 tests, 0 failures    --tap    フラグを使えば、Test Anything Protocolに対応するテキストでテスト情報を取得できます。おびただしい数のプログラムへのプラグインを、     Jenkins    、     Redmine    、     SublimeText    などで見つけられます。
独特のテスト構文以外にも、     bats    には興味深い特徴があります。
runコマンドで、まずコマンドを実行し、その結果とテキスト出力をテストできます。その際は、専用の変数、$statusと$outputを使います。loadコマンドで、共通のコードベースをロードできます。skipコマンドで、必要な時にテストをスキップできます。setup()関数とteardown()関数で、環境を調整したり、後にクリーンアップするように設定したりすることができます。- 特定の変数が完全に揃っています。
 - フォルダ内で全テストを実行できます。
 - コミュニティが活発です。
 
    bats    の利点をたくさん挙げてきました。欠点については、私が指摘できるのは1つだけです。
batsは正当なbashからはほど遠いこと。テストは、異なるシバンを使って.bats拡張子を持つファイル内に書く必要があります。
結論: 欠点なしに近い高品質なツール。強く勧めます。
結果
このリサーチは、私の個人的なプロジェクト、     git-secret    のために質の良いテストを書く試みとして行いました。その主な目標は暗号化されたファイルを     git    リポジトリに保存することです。チェックしてみてください。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- X: @yosuke_furukawa
 - Github: yosuke-furukawa
 

