次の方法で共有



March 2009

Volume 24 Number 03

.NET との相互運用性 - IronRuby を使用して受け入れテストを自動化する

Ben Hall | March 2009

コードは MSDN コード ギャラリーからダウンロードできます。
オンラインでのコードの参照

この記事では、次の内容について説明します。

  • 受け入れテストとは
  • 自動受け入れテスト
  • 受け入れテストを実装する
この記事では、次のテクノロジを使用しています。
IronRuby

目次

受け入れテストとは
自動受け入れテスト
ストーリー
シナリオ
受け入れテストを実装する
RSpec シナリオ ランナーを使用して C# オブジェクトを操作する
UI の受け入れテスト
今後の予定

ほぼすべての開発者は、顧客要件が適切に伝わり、不完全な機能や不正確な機能の実装に無駄な時間を費やさずに済むことを望んでいます。明確でわかりやすい仕様書を顧客に提示し、顧客の機能要件に合致しているかどうかを確認できればすばらしいことです。そうすれば、余分な労力をかけずにまったく同じ仕様で実行し、実装を要件に照らして検証することが可能になります。

受け入れテストと実行可能な仕様の概念を取り入れることにより、このように高度な顧客と開発者のコミュニケーションが実現します。ビヘイビア駆動開発 (BDD: Behavior Driven Development) を利用して、要件をより効果的に伝えられるようにすることができます。

MSDN Magazine 2009 年 2 月号の記事 (「IronRuby と RSpec の概要 (第 1 部)」) では、IronRuby を紹介し、動的言語である Ruby を使用して C# などの .NET 対応コードとの相互運用性を確保する方法を例を挙げて説明しました。今回の記事では、受け入れテストの概念について説明します。また、前回の記事で説明した概念に基づいて、IronRuby と RSpec を使用して受け入れテストを自動化し、.NET アプリケーションを検証する方法と、システムの実行可能な仕様を作成する方法を示します。

受け入れテストとは

受け入れテストという用語からはさまざまな定義が連想されますが、個人的には、受け入れテストとは、コードのバグ削減よりも、開発中のシステムが顧客要件を満たしているかどうかの検証を重視したテスト手法であると考えています。つまり、受け入れテストの核心は、コードをテストすることではなく、顧客または業務にとって必要な機能を構築することです。言うまでもありませんが、多くのプロジェクトが失敗する主な原因は、受け入れテストが不十分であること、また、要件の把握が不十分であることです。

受け入れテストでは、顧客または少なくとも顧客の代理人の協力を得て受け入れ基準を定義することが不可欠です。だれかが受け入れ基準の定義を促進しないと、ソフトウェアの動作が適切であるかどうかを検証する基準がないということになり、構築中のソフトウェアの的確性を確認することが困難です。顧客と開発チームが協力して、要件とその実現方法を規定するさまざまなテスト シナリオでシステムを定義する必要があります。

たとえば、e コマース システムを開発する場合、ショッピング カートの操作に基づいて受け入れテストを行う方法があります。一般的なシナリオとして、"空のショッピング カートに製品 123 を追加すると、品目数が 1 増えて、小計は 20 ドルになる" のような例が考えられます。このシナリオで、顧客が期待するショッピング カートの動作が明確になり、実装を検証するためのいくつかの手順が得られます。システムの成長に伴い、システムのさまざまな部分や機能セットを検証するシナリオも増えていきます。

シナリオを作成するときは、使用する言語を検討することも重要です。言語は、開発者によるソリューションの実装方法ではなく、業務上の問題のとらえ方を反映する必要があります。テストを実際の実装の観点から説明すると、"[送信] ボタンをクリックすると [確認] ラベルの表示が変わる" のようになりますが、これでは顧客にとってはあまり意味がなく、テストはシステムの実際の実装方法に依存することになります。実装に依存したテストでは、実際の実装を変更した場合、ビジネス要件が変わっていないにもかかわらず、関連したテストを更新する必要があるため、余分な保守コストがかかります。

テストに明確な要件と成功基準を規定することにより、顧客の期待に応えるソフトウェアを実現できる可能性がはるかに高まります。ただし、この場合にも、要件が満たされていること、およびアプリケーションが期待どおりに動作することを人が介在して手動で検証する必要があります。そこで自動受け入れテストという考え方が登場します。最新状態でないファイル共有上の文書に要件を保存するのではなく、例やシナリオとして要件を定義し、実装の成果物と共にソース管理にチェックインすれば、いつでも実行して、要件が実装されているかどうか、およびその実装が正しく機能しているかどうかを確認できます。テストも同じ方法で作成できますが、その場合、テスト ケース管理ソフトウェアやスプレッドシートで作成するのではなく、コードに直接記述します。

自動受け入れテスト

受け入れテストは顧客の要望どおりのアプリケーションが構築されていることを確認するのに役立ちます。また、これらのテスト シナリオを自動化することにより、開発プロセスの全段階でいつでも実装を確認できるようになります。自動化したシナリオは、将来の変更が現在の要件に違反することを防ぐための回帰テスト スイートの一部として使用できます。

ただし、テストの作成に顧客が関与する場合、特にテストを自動化する場合には、いくつかの潜在的な問題があります。一般に、顧客は技術に精通しておらず、実際のソフトウェア開発に関与することを敬遠する傾向があります。このような場合に技術チームの協力があれば、顧客から情報や例を入手することが可能になり、テスト担当者や開発者もシナリオや実行可能な仕様を迅速にコード化できます。また、シナリオは業務関係者全員にとってわかりやすいものである必要があります。IronRuby を使用することにより、テストの可読性は向上します。では、そのしくみはどうなっているのでしょうか。

このプロセスを実際のプロジェクトで使用する場合に考えられる使用例を示すため、価格計算システムに関するあるユーザー ストーリーの実装を説明します。例は単純ですが、必要な手順はわかると思います。受け入れテストは問題へのアプローチ方法を示すフレームワークであり、バリエーションや解釈は多様ですが、具体的にどのような手法が適しているかを判断するうえで役立つ基本概念が得られます。

これから紹介する例は、地元のユーザー グループを対象に筆者が行った「Red, Green, Refactor (Red/Green/Refactor)」というプレゼンテーションがきっかけになって思いついたものです。プレゼンテーションの後、出席者の 1 人から、アプリケーションの単体テストで、パッケージへのさまざまなオプションや追加料金を含めた価格計算に間違いがないことを確認する効果的な方法についてアドバイスを求められました。これは受け入れテストの有用性を示すのに最適な例です。価格と期待される処理に関するストーリーとシナリオを作成することによって、システムが正しく動作していることを確信できます。

また、そのような検証用のストーリーとシナリオは、開発チームと顧客の両者にとって、システムのさまざまな動作例や例外を記述した有益な資料になります。顧客が価格の問題を持ち出した場合は、開発チームは顧客が承認した実行可能な仕様を完了した作業の根拠として示すことができます。

これらのテストを実施しない場合、価格に関するルールはさまざまな価格構成の処理方法を説明する膨大な Word 文書に定義される可能性が高くなりますが、このような文書は開発中のシステムとは本質的に切り離されています。自動受け入れテストを支持する理由はここにあります。

ストーリー

受け入れテストを実施することを前提にして価格計算システムを開発する場合の手順は、関係者全員が集まってストーリーを定義することから始まります。顧客は、チームの技術担当者の助けを借りて要件を定義します。前回の記事で説明したように、BDD では、チームのメンバ間で使う言語を統一できるように、あらかじめ決められたフォーマットでストーリーを定義し、顧客の要件を具体的に記述します。次のフォーマットに従って、顧客の協力を得ながら詳細を記入します。

As a [要求者の役職] I want [要求する機能] so that [目的].

犯しやすい誤りは、ストーリーに実装の詳細を含めようとすることです。たとえば、

As a [管理者], I want [default.aspx のグリッド ビューに価格を表示する] so that [価格のコストを入力する]

ストーリーには、技術的な詳細は避けて、業務で使用する用語のみで問題を記述する必要があります。改善した例を次に示します。

As a [販売管理者], I want [顧客に適用する価格を確認する] so that [価格のコストを入力する]

この記述はフォーマットに従ってはいますが、情報がきわめてあいまいで、どこから実装に着手したらよいかがわかりません。さらに改善した例を次に示します。

As a [販売管理者], I want [製品 x の価格を表示する] so that [要件に基づいて顧客に正確なコストを提示する]

これで、ストーリーが具体的になり、顧客の要件と実際に要求される機能が明確になりました。システムの実装時には、この方がはるかに役立ちます。

ストーリーおよび機能を記述するうえで重要なことは、できるだけ焦点を絞るということです。これにより、シナリオの作成、テストの作成、および完成したコードの実装が容易になります。システムによっては、機能に基づいてストーリーを作成することも考えられます。

シナリオ

ストーリー以外にも、期待される機能に関する詳細な情報が必要です。それにはシナリオが非常に役立ちます。シナリオは "一定の条件で、何かが発生したとき、結果はこうなる" ということを規定します。目的は、さまざまな状況で動作するストーリーの実装方法について具体的な例を示すことです。これによって、期待される機能がチームに伝わり、確認のステップも明確になります。

推奨される構文は、Dan North によって提唱された Given、When、Then (GWT) 構文です。

Given [条件], When [何かが発生], Then [結果].

シナリオの例を次に示します。

シナリオ: 製品 X のシングル ユーザー ライセンス (サポートなし)

Given [製品 X], When [ユーザーがシングル ユーザー ライセンスを要求] And [サポートを希望しない], Then [価格は 250 ドル]

"And" という言葉で複数の条件または結果を連結してシナリオに意味を追加し、発生する現象に関する情報を追加できます。

目標は、できるだけ多くのシナリオを指定してあいまいさを解消すること、また、実行可能な仕様として役立て、初期作業を開始するために十分な情報を規定することです。シナリオは固定的なものではないことに注意してください。作業の進行に伴って情報が追加された場合は、既存のシナリオについて顧客に質問することや、追加された情報に基づいて追加シナリオを作成する必要がある場合もあります。

シナリオを作成する際にはいくつかの注意点があります。ストーリーと同様、シナリオに記述する例は 1 つに絞ります。その主な理由は読みやすくするためです。シナリオの情報が多すぎると、中心となるメッセージが不明になります。

受け入れテストを実装する

ストーリーとシナリオを明快でわかりやすいフォーマットで記述したら、ストーリーとシナリオを自動化します。自動化することにより、ストーリーやシナリオを開発時に実行し、進行状況を追跡したり、回帰バグを捕捉することが可能になります。この記事では RSpec に基づいて説明しますが、基本的なプロセスはあらゆる BDD フレームワークに通用します。IronRuby および RSpec のインストールの詳細については、前述の記事を参照してください。

前の例に従ってストーリーを作成する場合は、RSpec のシナリオ ランナーを使用するのが無難です。空のプレーン ファイルに、ここでは pricing_scenarios_for_product_x という名前を付けて、ストーリーとシナリオを次の形式でコピーします。

Story: Pricing for New Product X
  As a sales administrator, I want to be able to view
  prices for product x so that I can provide customers
  with an accurate cost for their requirements. 
Scenario: Single User License for Product X without support
  Given Product X
  When user requests a 1 user license
  And this does not include support
  Then the price should be $250

これがアプリケーションの検証時に使用する受け入れ基準の基本になります。ただし、これらのストーリーとシナリオを実行可能な仕様として使用するには、ストーリーおよびシナリオを実行するための Ruby コードが必要です。IronRuby はコードの作成と実行を行う簡易環境です。必要な作業は、.rb 拡張子のファイルを作成して RSpec テストを記述することだけです。

まず、RSpec フレームワークを参照します。それには、require ディレクティブを使用します。次の 2 行を追加すれば、RSpec ストーリー ランナーを使用できるようになります。

require 'rubygems'
require 'spec/story'

これでステップを定義できるようになりました。ステップとは、GWT フォーマットを使用したシナリオの各行を指します。RSpec では、ステップはコードのメソッドに関連付けられます。各ステップを 1 つのタスクのみに関連付けます。たとえば、Given ステップをオブジェクトの設定に使用し、Then では一般にアサーションと検証を行います。

RSpec では、すべてのステップを steps_for ブロックでラップする必要があります。かっこ内は一連のステップの識別子です。

steps_for(:product_x) do
  #Steps go here
end

ストーリー内の各ステップは、シナリオの 1 行に直接関連しています。たとえば、RSpec コードの "Given Product X" の行は次のステップ メソッドに対応します。

Given("Product $productName at $price") do |productName, price|
   pending "Need to complete implementation for accessing C# object"
 end

RSpec の文字列操作機能により、開発者はステップ内でプレースホルダを使用し、プレースホルダの値を変数としてステップに設定することができます。この場合、ProductName と Price は変数に保存され、同じステップを複数の異なる製品および基本価格に再利用できます。

When ステップと Then ステップも同じ方法で記述します。

  When("user requests a $amount user license") do |amount|
    pending "Need to complete implementation for accessing C# object"
  end
  When("this does not include support") do
    pending "Need to complete implementation for accessing C# object"
  end
  Then("the price should be $price") do |price|
    pending "Need to complete implementation for accessing C# object"
  End

2 番目のステップは処理が常に同じなので、プレースホルダは不要です。したがって、このブロックにはパラメータがありません。テストを実行すると、RSpec はシナリオに基づいて適切なメソッドを適切な順序で呼び出します。値は定義に従って置き換えられます。

次に、先に作成したストーリー ファイルにパスを指定する必要があります。この処理は with_steps_for ブロックで行い、ステップを定義する際に使用する識別子を指定します。このブロックの本文では、シナリオの保存先のパスとファイル名を指定して run メソッドを呼び出します。

with_steps_for(:product_x) do
  run File.dirname(__FILE__) + "/pricing_scenarios_for_product_x"
end

テストを実行するには、IronRuby コマンド ライン ツール (ir) を実行し、Ruby ファイルをパラメータとして渡します。

>ir pricing_scenarios_for_product_x_story.rb

ご覧のとおり、これまでに紹介したステップの中で pending メソッドを呼び出しています。これは、機能の完成に必要な処理がまだ残っていることを示します。そのため、テストを実行すると、保留中のすべてのタスクが出力されます (図 1 を参照)。結果は解読しやすく、動作内容がひとめでわかります。実装が存在しないステップが成功と判定されることは避ける必要があります。

図 1 保留中のメソッドを表示する

Running 1 scenarios
Story: Pricing for New Product X
 As a sales administrator, I want to be able to view prices for product x, 
 so that I can provide customers with an accurate cost for their requirements.
 Scenario: Single User License for Product X without support
  Given Product X at 250 (PENDING)
  When user orders a 1 user license (PENDING)
  And this does not include support (PENDING)
  Then the price should be 250 (PENDING)
1 scenarios: 0 succeeded, 0 failed, 1 pending
Pending Steps:
1. Pricing for New Product X (Single User License for Product X without support): Product $productName at $price
2. Pricing for New Product X (Single User License for Product X without support): user orders a $amount user license
3. Pricing for New Product X (Single User License for Product X without support): this does not include support
4. Pricing for New Product X (Single User License for Product X without support): the price should be $price

RSpec シナリオ ランナーを使用して C# オブジェクトを操作する

これで、ストーリー、シナリオ、およびステップの実装は完了しましたが、システムを操作するコードはまだ実装されていません。前回の記事では、RSpec 仕様フレームワークの使用方法に重点を置いて C# オブジェクトの動作方法の例を示し、実装を分離された単体として検証しました。今回の記事では、分離されたブロックではなく、アプリケーション スタック全体を検証する場合に重点を置いて、RSpec ストーリー ランナーを受け入れテストに使用する方法を説明します。

まず、C# アセンブリを参照します。IronRuby は Ruby 言語のコンストラクタに対応しているため、C# ライブラリの参照方法は Ruby ライブラリの参照と同じです。

require File.dirname(__FILE__) + 
  '/CSharpAssembly/CSharpAssembly/bin/Debug/CSharpAssembly.dll'

次に、前のセクションで定義したシナリオの本文を記述します。シナリオがテストに合格するためには、注文の合計価格を計算する機能を実装する必要があります。Given ブロック内で、シナリオに必要なオブジェクトを初期化します。サンプル シナリオでは、この時点で初期化する必要があるオブジェクトは Product オブジェクトのみです。オブジェクトのコンストラクタには製品名と価格が必要です。製品名と価格はメソッド パラメータから取得され、メソッド パラメータはシナリオ自体から取得されます。

  Given("Product $productName at $price") do |productName, price|
    @product = CSharpNamespace::Product.new(productName, price.to_i)
  end

初期オブジェクトを取得したら、テストのサブジェクトとなるこれらのオブジェクトの変数の状態を設定する必要があります。この処理は When ブロックで行います。シナリオではユーザーが一定数のライセンスを注文したことを規定しているので、ステップにそれを反映し、それに従ってオブジェクトの状態を設定する必要があります。

  When("user orders a $amount user license") do |amount|
    @order = CSharpNamespace::Order.new(@product)
    @order.NumberOfLicenses = amount.to_i
  end

次に、前述の処理が正しく実行されることを確認します。そのために、Then ブロックに期待される機能とアサーションを定義します。

ここでは、作成した注文を取得し、小計がシナリオに定義した価格と一致していることを確認します。should は、すべてのオブジェクトに対して RSpec が動的に作成する拡張メソッドであり、ステートメントの真偽を確認する機能です。true ではない場合、例外が生成されます。

  Then("the price should be $price") do |price|
    @order.Subtotal.should == price.to_i
  end

これで、次のコマンドを使用してストーリーを実行できます。

>ir pricing_scenarios_for_product_x_story.rb

各シナリオを実行すると、ストーリーとシナリオがコンソールに出力されます。いずれかのステップが失敗すると、特定の問題が強調表示され、最後に合計が示されます。

Running 1 scenarios
Story: Pricing for New Product X
  As a sales administrator, I want to be able to view prices for product x, 
  so that I can provide customers with an accurate cost for their requirements.

  Scenario: Single User License for Product X without support

    Given Product X at 250
    When user orders a 1 user license
    And this does not include support
    Then the price should be 250 (FAILED)

1 scenarios: 0 succeeded, 1 failed, 0 pending

すべてのステップが正常に終了した場合、コマンド ラインに次のように表示されます。

Running 1 scenarios
Story: Pricing for New Product X
 As a sales administrator, I want to be able to view prices for product x, 
 so that I can provide customers with an accurate cost for their requirements.
 Scenario: Single User License for Product X without support
  Given Product X at 250
  When user orders a 1 user license
  And this does not include support
  Then the price should be 250
1 scenarios: 1 succeeded, 0 failed, 0 pending

この時点で、より複雑なシナリオを追加作成し、ビジネス ロジックを含めることができます。たとえば、価格に対する割引を含めると、シナリオは次のようになります。

Scenario: Single User License for Product X with 20% discount 
and includes 2 years unlimited premium support.
   Given Product X
   When user requests a 1 user license
   And including 2 years support
   And support length is unlimited
   And support type is premium
   And with 20% discount
   Then the price should be $800

前述のように、これらのシナリオでは業務上の用語を使用し、顧客をはじめ、業務関係者全員が理解できる言葉で記述します。

UI の受け入れテスト

例では、ロジックが正常に動作しているかどうかをテストするために、受け入れテストの焦点をビジネス ロジックとドメイン オブジェクトに絞っています。では、ユーザーのアプリケーション操作についてはどうでしょうか。これについても受け入れテストを用意して、ユーザーの観点からロジックが正しいことを確認する必要があります。ユーザーの観点とは、つまり UI です。

個人的には、受け入れテストでは、アプリケーション ロジックと、処理内容を決定するコードの重要なセクションに焦点を当てる必要があると考えています。アプリケーションにロジックと UI のコードを分離する優れた機能があれば、テストの実装は容易になります。このレベルでテストする場合には UI の変更に煩わされることはありません。

ロジックに限定してテストするからといって、UI に関する受け入れテストが不要というわけではありません。個人的には、UI のコア "ハッピー パス" をターゲットにした一連のスモーク テストを好んで利用しています。このテストでは、最小限のテストで最大限の成果を得るために、アプリケーションの中でもユーザーが特に一般的に使用する部分に重点を置きます。可能性があるすべてのパスを対象にして UI を使用した場合、オプションを別のダイアログに移動するなどの UI の変更が行われると、テストもすべて変更する必要があります。

たとえば、e コマース サイトの UI をテストする場合、項目を選択し、その項目をショッピング カートに追加し、チェックアウトして、購入の確認画面を表示するというハッピー パスが考えられます。このシナリオが失敗した場合、できるだけ早急に通知を受けることが求められます。

アプリケーションの複雑さや有効期間によっては、UI に関する受け入れテストの比重を高めて UI 層の高信頼性を確保することも有益です。UI を効果的にテストする方法は難しい課題ですが、スペースが足りないので、ここでは説明を省きます。筆者の、WPF のテストを対象とする Project White に関するブログおよび Web アプリケーションのテストを対象とする WatiN に関するブログを一読されることをお勧めします。また、MSDN Magazine 2007 年 12 月号の James McCaffrey による「Windows PowerShell での UI テスト自動化」も参照してください。

今後の予定

IronRuby を使用して受け入れテストを開始する準備はこれで完了です。テスト開始に必要な情報は揃っているはずですが、いくつかの注意点を付け加えておきます。

潜在的な問題の 1 つは、IronRuby は実稼動環境で使用するにはまだ時期尚早だということです。現在でも実稼動環境で使用できないことはありませんが、IronRuby および RSpec はいずれも開発途上で、改良が繰り返されています。今のところ、バグ修正のレベルですが、今後、互換性に影響するような変更が行われる可能性もあります。RSpec の実行速度に関する問題もあります。ただし、この問題とテストが簡素化されるメリットとどちらが大きいかを評価する価値はあります。

また、コンテキスト切り替えについても一部に懸念の声があります。一般に、テストの作成と実稼動コードの作成には同じ言語を使用することが好まれます。基本的にはこの考えに賛成ですが、日常的に膨大な回数の切り替えを行う単体テストには IronRuby や RSpec は使用しない方が無難でしょう。受け入れテストの場合はコンテキスト切り替えの回数がはるかに少なくなります。この場合、わかりやすく、検証やシナリオをより自然な方法で記述できるという利点が顧客および開発チームにもたらす多大なメリットを考えれば、コンテキスト切り替えをあまり問題にする必要はないでしょう。

また、受け入れテストを開発プロセスに組み込むことは、開発組織に大きなメリットがあります。ストーリーやシナリオなどの手法によって顧客にわかりやすい言葉で機能を定義し、理解しやすい自動受け入れテストを取り入れることにより、顧客との信頼関係が強化され、一貫して堅牢で信頼性の高いソフトウェアを提供することができます。

Ben Hall は、ソフトウェア開発に並々ならぬ情熱を抱いている英国の C# 開発者/テスタです。コード記述の愛好家でもあります。Red Gate Software 社にテスト エンジニアとして勤務し、手動、自動を問わず、ソフトウェア テストのさまざまな手法を模索しています。特に力を入れているのは、種類の異なるアプリケーションの最適なテスト手法の研究です。Ben は C# MVP です。彼のブログは Blog.BenHall.me.uk で読むことができます。