1960年代、ソフトウェア工学では、コードを書きながらテストすることが基本中の基本とされ、良い習慣とされていました。 その時代のソフトウェア開発のパイオニアたちは、さまざまなレベルのテストの支持者でした。ある者は「ユニット」テストを提唱し、ある者はそうしませんでしたが、全員がコードをテストすることの重要性を認識していました。 彼女はこれを「高次ソフトウェア」と呼び、問題領域に対して直接動作するのではなく、他のソフトウェアに対して動作するソフトウェアを意味しました。 彼女の高次ソフトウェアは、統合の問題につながることが知られているパターンを探すためにソースコードを調べました

1970年までに、人々は実行可能テストについてほとんど忘れていました。 確かに、人々はアプリケーションを実行し、手作業であちこちを突いていましたが、ビルが周囲で燃え尽きない限り、コードは「十分良い」と考えていました。 その結果、35 年以上にわたって、不適切にテストされ、多くの場合、意図したとおりに、あるいは顧客を満足させる方法で完全に動作しないコードが世界中で生産され続けてきたのです。 インフラストラクチャ エンジニアやシステム管理者は、プログラマーがアプリケーション コードをテストするよりもさらに熱心にスクリプトをテストしません。

多数の自律したコンポーネントからなる複雑なソリューションの迅速な展開が標準となり、「クラウド」インフラストラクチャでは、手動では管理できない規模で、何千もの行き来のある VM やコンテナを管理しなければならない時代になると、開発および配信プロセスを通じて、実行可能で自動的に実行できるテストとチェックが重要になり、アプリケーション プログラマのみならず IT 関連のすべての人にとって無視できない存在になっています。

デボップス (開発と運用のスキル、手法、およびツールの掛け合わせ) の登場や、「インフラストラクチャ アズ コード」、「あらゆるものを自動化」といったトレンドにより、ユニット テストはプログラマ、テスター、システム管理者、インフラエンジニアにとって同様に基本スキルになってきました。

この連載では、シェル スクリプトのユニット テストというアイデアを紹介し、次に、このタスクを実用的かつ持続可能にするのに役立ついくつかのユニット テスト フレームワークを探ります。 このシリーズの後半では、アプリケーション開発者が使用し、インフラストラクチャ エンジニアにとっても効果的で有用なバージョン管理システムとワークフローについて触れます。

テストするスクリプト

Vivek Gite は、ディスク使用量を監視し、特定のファイルシステムが閾値を超えるとメール通知を生成するサンプル シェル スクリプトを発表しました。 彼の記事はこちらです。 https://www.cyberciti.biz/tips/shell-script-to-watch-the-disk-space.html.

彼のスクリプトの初期バージョンは、Per Lindahl からのコメントで提案されたように、出力の改行を防ぐために df コマンドに -P オプションを追加して、次のようになります:

#!/bin/shdf -HP | grep -vE '^Filesystem|tmpfs|cdrom' | awk '{ print  " "  }' | while read output;do usep=$(echo $output | awk '{ print }' | cut -d'%' -f1 ) partition=$(echo $output | awk '{ print  }' ) if ; then echo "Running out of space \"$partition ($usep%)\" on $(hostname) as on $(date)" | mail -s "Alert: Almost out of disk space $usep%" [email protected] fidone

Vivek はこの時点以降もスクリプトを改良しますが、このバージョンで今回の投稿の目的に対応します。

自動化された機能チェック

アプリケーション コードやスクリプト、その他のあらゆる種類のソフトウェアをチェックする場合でも、自動化された機能チェックに関する経験則をいくつか紹介します:

  • チェックは毎回同じように実行しなければならず、各実行の準備として手動で調整する必要はない。

Pass, Fail, and Error

スクリプトがまったく実行されない可能性を指摘されるかもしれません。 それは、どのような種類のアプリケーションのユニット テスト フレームワークでも普通です。

  • The code under test exhibits the expected behavior
  • The code under test runs, but doesn’t exhibit the expected behavior
  • The code under test does not run

実用的には、三番目の結果は二番目の結果と同じで、何が間違っていたのかを見つけて修正しなければなりません。 ですから、私たちは一般に、こうしたことをバイナリとして考えます。

What Should We Check?

この場合、さまざまな入力値でスクリプトが期待通りに動作することを確認することに関心があります。

テスト中のコードを見直すと、ディスク使用率が 90% のしきい値に達すると、スクリプトがメールを呼び出してシステム管理者に通知を送信することがわかります。

ユニット チェックの一般的に受け入れられているグッド プラクティスに沿って、各初期条件のセットに対して期待される各動作を検証するために、個別のケースを定義したいと思います。 ディスク使用率の多数の異なるパーセンテージを個別にチェックする必要はありません。 私たちは、境界での動作をチェックする必要があるだけです。 したがって、意味のあるカバレッジを提供するためのケースの最小セットは次のようになります:

  • It sends an email when disk usage reaches the threshold
  • It does not send an email when disk usage is below the threshold

What Should We Not Check?

In keeping with general accepted good practice for unit test isolation, we want to ensure each of our cases can fail for exactly one reason: 期待された動作が起きないからです。 実用的な範囲では、他の要因によってケースが失敗しないように、チェックをセットアップしたいと思います。

外部要因が自動化されたチェックに影響しないことを保証することは、常にコスト効率が良いとは限りません (あるいは可能であるとも限りません)。 外部要素を制御できない場合、または制御するとチェックの価値よりも多くの時間、労力、およびコストがかかる場合、発生する確率が非常に低い、または発生しても影響が非常に小さい、不明瞭なエッジ ケースが含まれる場合があります。 これは、あなたの専門的な判断の問題です。 一般的なルールとして、テスト中のコードの範囲を超えた要因に依存するものを作らないよう、最善を尽くしてください。

私たちは df、grep、awk、cut、および mail コマンドが動作することを確認する必要はありません。 それは私たちの目的の範囲外です。 ユーティリティを保守している人がその責任を負うことになります。

私たちは、df コマンドからの出力が grep や awk で期待したとおりに処理されないかどうかを知りたいと思います。 したがって、各テストケースの意図に一致する df コマンドからの出力に基づいて、実際の grep および awk コマンドをチェックで実行させたいと考えています。 df へのコマンドライン引数はスクリプトの一部であり、スクリプトはテスト対象のコードであるため、これはスコープ内です。

つまり、ユニット チェックで使用するために、df コマンドの偽バージョンが必要だということです。 その種の偽のコンポーネントは、しばしばモックと呼ばれます。 モックは実際のコンポーネントの代わりとなり、制御された方法でシステムの動作を駆動するために事前定義された出力を提供し、テスト中のコードの動作を確実にチェックできます。 ユニットチェックで無駄な電子メールを大量に送り出したくないので、mail コマンドもモックにしたいと思います。

このスクリプトは、これらのコマンドのモックを説明する良い例です。 スクリプトの関連する行は次のとおりです:

df -HP | grep -vE '^Filesystem|tmpfs|cdrom' | awk '{ print  " "  }'

もし grep にパイプを通さずに df -HP だけを実行した場合、次のような出力になります:

Filesystem Size Used Avail Use% Mounted onudev 492M 0 492M 0% /devtmpfs 103M 6.0M 97M 6% /run/dev/sda1 20G 9.9G 9.2G 52% /tmpfs 511M 44M 468M 9% /dev/shmtmpfs 5.3M 0 5.3M 0% /run/locktmpfs 511M 0 511M 0% /sys/fs/cgrouptmpfs 103M 8.2k 103M 1% /run/user/1000

grep と awk コマンドは出力を次のように縮小します:

0% udev52% /dev/sda1

我々のテストケースは df からの出力も制御しなければなりません。 テストスイートを実行しているシステムの実際のディスク使用量に基づいて、チェックの結果が変化することは避けなければなりません。 ディスク使用量をチェックしているのではなく、スクリプトのロジックをチェックしているのです。 スクリプトが本番環境で実行されると、ディスク使用量がチェックされます。 ここでやっているのは検証のためで、本番運用ではありません。 したがって、各ケースの「テスト データ」を生成できる、偽または「モック」の df コマンドが必要です。

*nix プラットフォームでは、エイリアスを定義することにより、実際の df コマンドをオーバーライドすることが可能です。 エイリアスのコマンドは、df -HPからの出力と同じフォーマットでテスト値を出力するようにしたいのです。 以下はその方法の 1 つです (これはすべて 1 行です。読みやすくするため、以下では分割しています):

alias df="shift;echo -e 'Filesystem Size Used Avail Use% Mounted on'; echo -e 'tempfs 511M 31M 481M 6% /dev/shm'; echo -e '/dev/sda1 20G 9.9G 9.2G 52% /'"

shift はスクリプトの実行時に ‘-HP’ 引数をスキップするので、システムは -HP が未知のコマンドであると文句を言うことがありません。 エイリアスの df コマンドは df -HP.

テスト値はスクリプトの実行時に grep と awk にパイプされるので、テストケースを制御するために必要な最小限のものだけをモックしています。 テストケースは、誤検出が起こらないように、できるだけ「本物」に近いものにしたいのです。

モッキング ライブラリ経由で作成されたモックは、呼び出されるとあらかじめ定義された値を返すことができます。 テスト対象のコードが df を呼び出すたびに返される定義済みの出力を指定しています。

Mocking the mail Command

正しい条件でスクリプトが電子メールを送信しようとするかどうかを知りたいですが、どこにも実際の電子メールを送信させたくはありません。 したがって、以前にdfコマンドを行ったように、mailコマンドをエイリアス化したい。 各テストケースの後にチェックできるものをセットアップする必要がある。 一つの可能性は、mailが呼ばれたときにファイルに値を書き、テストケースの中でその値をチェックすることです。 これは、以下の例で示されています。

モッキングライブラリで作成したモックは、テスト対象のコードから呼び出される回数をカウントし、予想される呼び出し回数を主張することができます。 diskusage.sh スクリプトを実行した後、ファイル mailsent にテキスト “mail” が存在する場合、スクリプトが mail コマンドを呼び出したことを意味します。

Pattern for Running Automated Checks

あらゆる抽象的なレベルで、あらゆる言語のアプリケーションまたはスクリプトに対して自動化または実行可能チェックを行うと、通常 3 段階で構成されます。

  • Arrange
  • Act
  • Assert

この理由は、おそらく誰もが音韻、特に「音韻」という言葉自体が「A」で始まっていることから「A」が大好きだからでしょう。 act のステップでは、テスト対象のコードを呼び出します。

テスト フレームワークまたはライブラリを使用する場合、ツールは私たちのためにアサート ステップをうまく処理するので、テスト スイートで多くの面倒な if/else ロジックをコーディングする必要はありません。 この最初の例では、テストフレームワークやライブラリを使用していないため、if/else ブロックで各ケースの結果をチェックします。

Vivek のシェル スクリプトをテストするための、粗いながらも効果的なテスト スクリプトを以下に示します。 それはまったく問題ありません。

次に、shopt コマンドが表示されます。 これは、diskusage.sh スクリプトを実行するためにサブシェルが呼び出されたときに、シェルがテストのエイリアスを展開するようにします。 ほとんどの使用例では、エイリアスをサブシェルに渡すことはありませんが、ユニット テストは例外です。

「すべての前に」というコメントは、セットアップとティアダウン コマンドがあるユニット テスト フレームワークに精通している人のためのものです。 これらは、しばしば「前」と「後」のような名前で呼ばれ、通常、テスト スイート全体を括る 1 つのペアと、各テスト ケースに対して個別に実行される別のペアがあります。

私たちは、メールのエイリアスを定義し、テスト結果ファイルを初期化し、テスト ケース カウンタを初期化することはすべてテスト スイートの最初で、まさに 1 回行われることを示したかったのです。 このようなことは、実行可能なテストスイートでは普通のことです。 アプリケーション プログラムではなく、シェル スクリプトをテストしているという事実は変わりません。

次のコメント、「何もしない…」は、最初の個別のテストケースの開始を示します。 ほとんどのユニット テスト フレームワークは、各ケースに名前を付ける方法を提供します。そのため、何が起こっているかを追跡でき、他のツールがさまざまな理由でテストケースを検索、フィルター、および抽出できます。 こちらは、1つのテストケースにのみ適用される設定を表しています。 この特定のケースに必要な出力を出すために、df のエイリアスを設定しています。 また、”no mail “というテキストをファイルに書き込んでいます。

次に来るのは、テスト中のコードを実行する行為ステップです。 この場合、diskusage.sh スクリプト自体を実行することを意味します。

次に、テストフレームワークをまだ導入していないため、この例では難しい方法で行っているアサートステップを行います。 テストカウンターをインクリメントして、結果ファイルのテストケースに番号を振ることができるようにします。 そうしないと、テストケースが大量にある場合、どれが失敗したのかを把握するのが難しくなります。

mail コマンドのために定義したエイリアスは、テキスト ‘mail’ を mailsent ファイルに書き込みます。 diskusage.sh が mail を呼び出すと、mailailsent ファイルには初期値である ‘no mail’ ではなく ‘mail’ が含まれることになります。

It sends an email notification…というコメントから始めて、別のテストケースについて arrange, act, assert のステップを繰り返します。

「すべて終了」コメントが表示されたところで、テスト スクリプトの先頭付近の「すべて開始」セットアップで作成した定義を削除して、自分自身の後始末をしています。

最後に、test_results ファイルの内容をダンプして、何が得られたかを確認します。 次のようになります。

Test results for diskusage.sh1. PASS: No action taken for disk usage under 90%2. PASS: Notification was sent for disk usage of 90%

Why Use a Test Framework/Library?

テスト フレームワーク、モッキング ライブラリ、またはアサーション ライブラリを使用せずに、シェル スクリプトのユニット テスト ケースをいくつか作成しました。 システム コマンドはエイリアスを定義することでモック化でき (少なくとも *nix システムでは)、アサーションは条件文として実装でき、ユニット テストの基本構造は手で簡単にセットアップできることがわかりました。 では、どのような利点があるのでしょうか。

テスト フレームワークおよびライブラリは、テスト コードを単純化および標準化し、多くの条件文を含む手作りのスクリプトよりもずっと読みやすいテスト スイートを可能にします。 一部のライブラリは、例外をトラップする機能や、テーブル駆動型およびデータ駆動型のテスト ケースを記述する機能など、便利な追加機能を備えています。 ChefやPuppetなど、インフラエンジニアが関心を持つ特定の製品をサポートするように調整されているものもある。 また、コード カバレッジを追跡する機能や、CI/CD パイプラインのツール、あるいは少なくとも Web ブラウザで消費可能な形式でテスト結果をフォーマットする機能を含むものもあります。

スクリプトのためのユニット テスト フレームワーク

このシリーズでは、シェル スクリプトやスクリプト言語用のユニット テスト フレームワークをいくつか見ていきたいと思います。 以下はその概要です:

  • shunit2 は 10 年の歴史を持つ、非常に堅実なオープン ソース プロジェクトです。 もともとは、チューリッヒに拠点を置く Google のサイト信頼性エンジニア兼マネージャーである Kate Ward によって開発され、6 人のチームによって活発に開発およびサポートされています。 シェルスクリプトのロギングライブラリをテストするためのポイントソリューションとして謙虚に始まり、複数のシェル言語とオペレーティングシステムをサポートする汎用のユニットテストフレームワークに意図的に発展してきました。 データ駆動型やテーブル駆動型のテストをサポートするなど、単純なアサーションにとどまらない便利な機能を多数搭載しています。 アサーションは伝統的な “assertThat “形式を採用している。 プロジェクトサイトには、優れたドキュメントが掲載されています。 シェルスクリプトの汎用ユニットテストのために、これは私の一番の推薦です。
  • BATS (Bash Automated Testing System) は bash 用のユニットテストフレームワークです。 これは約 7 年前に Sam Stephenson によって作成され、十数人の貢献者がいました。 最後の更新は4年前ですが、この種のツールは頻繁な更新やメンテナンスを必要としないので、これは心配する必要はありません。 BATS は Test Anything Protocol (TAP) をベースにしており、あらゆる種類のテストハーネスにおけるモジュール間の一貫したテキストベースのインタフェースを定義している。 このプロトコルによって、テストケースにきれいで一貫性のある構文を使うことができる。 例えば、アサーションのための特別な構文はありません。テスト結果を得るためにbashコマンドを書くのです。 このことを考えると、テストスイートとケースを論理的に整理することが、その主な価値といえるかもしれない。 また、bash でテストスクリプトを書いたからといって、bash 以外のスクリプトをテストできなくなるわけではありません。 BATSの構文がプレーンなbashの構文に非常に近いため、テストスイートでさまざまなシェル言語を柔軟に扱えるようになります。 特に興味深い機能として(私の意見ですが)、プロジェクトのwikiに記載されているように、BATS用のシンタックスハイライトを備えたテキストエディターをセットアップすることができます。 Emacs、Sublime Text 2、TextMate、Vim、そして Atom が、この投稿の日付の時点でサポートされています。
  • zunit (IBM のものではない、別のもの) は James Dinsdale が開発した zsh 用ユニットテスト・ フレームワークです。 プロジェクトサイトでは、zunit は BATS に触発されたと述べており、非常に有用な変数 $state、$output、および $lines が含まれています。 しかし、「assert actual matches expected」というパターンに従った決定的なアサーション構文も持っている。 これらのフレームワークはそれぞれ、いくつかのユニークな機能を持っています。 私が思うに、ZUnit の面白い機能は、アサーションを含まないテストケースに “危険” というフラグを立てることです。 これを上書きして強制的にケースを実行することもできますが、デフォルトでは、フレームワークは各テストケースにアサーションを含めることを忘れないようにサポートしています。 これは私の地味なサイドプロジェクトで、4 年以上続いており、少数の「本当の」ユーザーを抱えています。 現在、意図したとおりに動作しているため、あまり更新されていません。 このプロジェクトの目的の一つは、bashの関数を「流動的」なスタイルで使用することでした。 関数は順番に呼び出され、それぞれがそのタスクを実行するのに必要な数の引数を消費した後、次の関数に引数リスト全体を渡します。 その結果、”expect package-name to_be_installed” や “expect arrayname not to_contain value” などのステートメントを含む、読みやすいテストスイートができあがります。 テストファーストのスクリプト開発に使用すると、「モジュール性」「単一責任」「懸念の分離」(呼び方は自由)の考え方をサポートする関数を書くように開発者を導く傾向があり、結果としてメンテナンスが容易で再利用しやすい関数が得られます。 「
  • korn-spec は korn シェル用の bash-spec の移植版です。
  • Pester は Powershell 用のユニットテストフレームワークです。 Powershell は、純粋なスクリプト言語というよりも、アプリケーション プログラミング言語のように感じられ、Pester は完全に一貫した開発者エクスペリエンスを提供します。 PesterはWindows 10に同梱されており、Powershellをサポートする他のどのシステムにもインストールできます。 堅牢なアサーション・ライブラリ、モッキングの組み込みサポート、およびコード・カバレッジ・メトリックスを収集します。
  • ChefSpec は rspec をベースにして、Chef レシピ用の動作スタイルのテスト・フレームワークを提供します。 Chef は Ruby アプリケーションであり、ChefSpec は rspec の機能をフルに活用し、Chef 固有の機能をビルトインでサポートしています。
  • rspec-puppet は Puppet の動作スタイルのフレームワークで、機能的には ChefSpec と同様です。

admin

コメントを残す

メールアドレスが公開されることはありません。

lg