些細なスクリプト以上のものをbashで書くとき、コードをテスト可能にする方法をよく考えます。
値を取得して値を返す関数が少なく、環境内のある側面をチェックして設定し、ファイルシステムを変更する関数が多いため、bashコードのテストを作成するのは通常困難です。プログラムなどを呼び出す-環境に依存する、または副作用がある関数。したがって、セットアップとテストのコードは、テストするコードよりもはるかに複雑になります。
たとえば、テストする単純な関数について考えてみます。
function add_to_file() {
local f=$1
cat >> $f
sort -u $f -o $f
}
この関数のテストコードは、次のもので構成されます。
add_to_file.before:
foo
bar
baz
add_to_file.after:
bar
baz
foo
qux
そしてテストコード:
function test_add_to_file() {
cp add_to_file.{before,tmp}
add_to_file add_to_file.tmp
cmp add_to_file.{tmp,after} && echo pass || echo fail
rm add_to_file.tmp
}
ここでは、5行のコードが6行のテストコードと7行のデータによってテストされています。
ここで、もう少し複雑なケースを考えてみましょう。
function distribute() {
local file=$1 ; shift
local hosts=( "$@" )
for Host in "${hosts[@]}" ; do
rsync -ae ssh $file $Host:$file
done
}
そのためのテストを書き始める方法すら言えません...
それで、bashスクリプトでTDDを実行する良い方法はありますか、それともあきらめて他の場所に努力する必要がありますか?
これが私が学んだことです:
BashがTDDに適していないことはそれほど多くありませんが(他のいくつかの言語がより適していると思いますが)、Bashが使用される一般的なタスク(インストール、システム構成)は、テストを作成するのが困難です。 、特にテストのセットアップが難しい。
Bashでのデータ構造のサポートが不十分なため、ロジックを副作用から分離することが困難であり、実際、Bashスクリプトには通常ほとんどロジックがありません。そのため、スクリプトをテスト可能なチャンクに分割することは困難です。テストできる関数がいくつかありますが、それは例外であり、ルールではありません。
機能は良いこと(tm)ですが、これまでのところしか実行できません。
入れ子関数はさらに優れている可能性がありますが、制限もあります。
結局のところ、多大な努力を払うことである程度のカバレッジを得ることができますが、コードのあまり面白くない部分をテストし、テストの大部分を古き良き(または悪い)手動テストとして維持します。
メタ:私は自分の質問に答える(そして受け入れる)ことに決めました。なぜなら、 SinanÜnür's (投票)と mouviciel (投票)のどちらかを選択できなかったからです。同様に有用で洞察に満ちています。注意したいのは Stefano Borini's 答え、最初は感心しなかったが、時間が経つにつれてそれを理解することを学んだ。また、彼の シェルスクリプトの設計パターンまたはベストプラクティスの回答 (投票済み)は役に立ちました。
テストと同時にコードを記述している場合は、パラメーター以外を使用せず、環境を変更しない関数を高くするようにしてください。つまり、関数をサブシェルで実行した方がよい場合は、テストが簡単になります。いくつかの引数を取り、stdoutまたはファイルに何かを出力するか、システム上で何かを実行しますが、呼び出し元は副作用を感じません。
はい、最終的にはグローバルなWORKING_DIR変数を渡す関数の大きなチェーンになりますが、これは、各関数が何を読み取って変更するかを追跡するタスクと比較すると、小さな不便です。ユニットテストを有効にすることも無料のボーナスです。
出力が必要な場合は最小限に抑えるようにしてください。少しのサブシェルの乱用は、物事をうまく分離しておくのに大いに役立ちます(パフォーマンスを犠牲にして)。
関数が呼び出される線形構造の代わりに、いくつかの環境を設定してから、他の環境が呼び出されます。これらはすべて1つのレベルで、最小限のデータで深い呼び出しツリーを取得しようとします。グローバル変数からの自主的な禁欲を採用する場合、bashでの返品は不便です...
TDDを必要とするほど大きなbashプログラムをコーディングする場合は、間違った言語を使用しています。
Bashプログラミングのベストプラクティスに関する以前の投稿を読むことをお勧めします。おそらく、bashプログラムをテスト可能にするのに役立つものが見つかるでしょうが、上記の私の記述は変わりません。
Meszarosが呼ぶものを書く 消費者テスト どの言語でも難しい。もう1つのアプローチは、rsyncなどのコマンドの動作を手動で検証してから、ネットワークにアクセスせずに特定の機能を証明する単体テストを作成することです。このわずかに変更された例では、スクリプトがキーワード「test」で実行された場合、$ runを使用して副作用を出力します。
function distribute {
local file=$1 ; shift
for Host in $@ ; do
$run rsync -ae ssh $file $Host:$file
done
}
if [[ $1 == "test" ]]; then
run="echo"
else
distribute schedule.txt $*
exit 0
fi
#
# Built-in self-tests
#
output=$(mktemp)
expected=$(mktemp)
set -e
trap "rm $got $expected" EXIT
distribute schedule.txt login1 login2 > $output
cat << EOF > $expected
rsync -ae ssh schedule.txt login1:schedule.txt
rsync -ae ssh schedule.txt login2:schedule.txt
EOF
diff $output $expected
echo -n '.'
echo; echo "PASS"
きゅうり/アルバをご覧になることをお勧めします。私にとってはかなりいい仕事をしました。
さらに、次のようなことを行うことで、必要なほぼすべてをスタブ化できます。
#
# code.sh
#
some_function_calling_some_external_binary()
{
if ! external_binary action_1; then
# ...
fi
if ! external_binary action_2; then
# ...
fi
}
#
# test.sh
#
# now for the test, simply stub your external binary:
external_binary()
{
if [ "$@" = "action_1" ]; then
# stub action_1
Elif [ "$@" = "action_2" ]; then
# stub action_2
else
external_binary $@
fi
}
高度なbashスクリプトガイド にはassert関数の例がありますが、これはより単純でより柔軟なassert関数です-$ *のevalを使用して任意の条件をテストするだけです。
assert() {
if ! eval $* ; then
echo
echo "===== Assertion failed: \"$*\" ====="
echo "File \"$0\", line:$LINENO line:${BASH_LINENO[*]}"
echo line:$(caller 0)
exit 99
fi
}
# e.g. USAGE:
assert [[ $r == 42 ]]
assert "((r==42))"
BASH_LINENOと呼び出し元のbash組み込みは、bashシェル固有です。
Outthentic フレームワークを見てください-任意のBashコードを実行し、正式なDSLを使用してstdoutを分析するシナリオを作成するように設計されています。このツールを使用して、Tdd /ブラックボックステストスイートを構築するのは非常に簡単です。