web-dev-qa-db-ja.com

Bashとテスト駆動開発

些細なスクリプト以上のものを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を実行する良い方法はありますか、それともあきらめて他の場所に努力する必要がありますか?

42
Chen Levy

これが私が学んだことです:

  1. いくつかの テストフレームワーク bashで書かれていて、bash用ですが...

  2. BashがTDDに適していないことはそれほど多くありませんが(他のいくつかの言語がより適していると思いますが)、Bashが使用される一般的なタスク(インストール、システム構成)は、テストを作成するのが困難です。 、特にテストのセットアップが難しい。

  3. Bashでのデータ構造のサポートが不十分なため、ロジックを副作用から分離することが困難であり、実際、Bashスクリプトには通常ほとんどロジックがありません。そのため、スクリプトをテスト可能なチャンクに分割することは困難です。テストできる関数がいくつかありますが、それは例外であり、ルールではありません。

  4. 機能は良いこと(tm)ですが、これまでのところしか実行できません。

  5. 入れ子関数はさらに優れている可能性がありますが、制限もあります。

  6. 結局のところ、多大な努力を払うことである程度のカバレッジを得ることができますが、コードのあまり面白くない部分をテストし、テストの大部分を古き良き(または悪い)手動テストとして維持します。

メタ:私は自分の質問に答える(そして受け入れる)ことに決めました。なぜなら、 SinanÜnür's (投票)と mouviciel (投票)のどちらかを選択できなかったからです。同様に有用で洞察に満ちています。注意したいのは Stefano Borini's 答え、最初は感心しなかったが、時間が経つにつれてそれを理解することを学んだ。また、彼の シェルスクリプトの設計パターンまたはベストプラクティスの回答 (投票済み)は役に立ちました。

35
Chen Levy

テストと同時にコードを記述している場合は、パラメーター以外を使用せず、環境を変更しない関数を高くするようにしてください。つまり、関数をサブシェルで実行した方がよい場合は、テストが簡単になります。いくつかの引数を取り、stdoutまたはファイルに何かを出力するか、システム上で何かを実行しますが、呼び出し元は副作用を感じません。

はい、最終的にはグローバルなWORKING_DIR変数を渡す関数の大きなチェーンになりますが、これは、各関数が何を読み取って変更するかを追跡するタスクと比較すると、小さな不便です。ユニットテストを有効にすることも無料のボーナスです。

出力が必要な場合は最小限に抑えるようにしてください。少しのサブシェルの乱用は、物事をうまく分離しておくのに大いに役立ちます(パフォーマンスを犠牲にして)。

関数が呼び出される線形構造の代わりに、いくつかの環境を設定してから、他の環境が呼び出されます。これらはすべて1つのレベルで、最小限のデータで深い呼び出しツリーを取得しようとします。グローバル変数からの自主的な禁欲を採用する場合、bashでの返品は不便です...

11
Eugene

実装の観点から、 shUnit2 または bats をお勧めします。

実用的な観点から、私はあきらめないことをお勧めします。私はbashスクリプトでTDDを使用しており、努力する価値があることを確認しています。

もちろん、私はコードの約2倍のテスト行を取得しますが、複雑なスクリプトを使用する場合、テストへの取り組みはかなりの投資になります。これは特に、クライアントがプロジェクトの終わり近くに気が変わっていくつかの要件を変更した場合に当てはまります。回帰テストスイートを持つことは、複雑なbashコードを変更する上で大きな助けになります。

9
mouviciel

TDDを必要とするほど大きなbashプログラムをコーディングする場合は、間違った言語を使用しています。

Bashプログラミングのベストプラクティスに関する以前の投稿を読むことをお勧めします。おそらく、bashプログラムをテスト可能にするのに役立つものが見つかるでしょうが、上記の私の記述は変わりません。

シェルスクリプトのデザインパターンまたはベストプラクティス

4
Stefano Borini

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"
3
eradman

きゅうり/アルバをご覧になることをお勧めします。私にとってはかなりいい仕事をしました。

さらに、次のようなことを行うことで、必要なほぼすべてをスタブ化できます。

#
# 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  
}
3
user2512361

高度な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シェル固有です。

0
gaoithe

Outthentic フレームワークを見てください-任意のBashコードを実行し、正式なDSLを使用してstdoutを分析するシナリオを作成するように設計されています。このツールを使用して、Tdd /ブラックボックステストスイートを構築するのは非常に簡単です。

0
Alexey Melezhik