June 2009

Volume 24 Number 06

テストの実行 - IronPython で .NET モジュールをテストする

James McCaffrey | June 2009

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

目次

テスト対象のモジュール
アドホックな対話型モジュール テスト
軽量モジュール テストの自動化
まとめ

Python スクリプト言語は、ある種のソフトウェア テストの実行に最適な機能を備えています。Python には、Cpython や IronPython を含む使用できる実装がいくつかあります。CPython は UNIX 系オペレーティング システムを実行するマシンで最も一般的な実装であり、IronPython は 2006 年末にリリースされた Microsoft .NET Framework 用の実装です。今月のコラムでは、Python コマンド ラインと軽量 Python スクリプトの両方の方法で、IronPython を使用して .NET ベースのモジュールをテストする方法を紹介します。読者が JavaScript、Windows PowerShell、VBScript、Perl、PHP、Ruby などのスクリプト言語の経験があるという前提で説明しますが、Python の経験はなくてもかまいません。どのような内容になるのかについては、図 1 を見てください。IronPython を使用して、2 枚のカードを比較する TwoCardPokerLib という名前の .NET モジュールのアドホックなテストを簡単に実行します。図 1 で示されているコマンドについては後で詳しく説明しますが、ここでは一時的に .NET モジュールが含まれる DLL に対する参照を対話形式で追加し、このモジュールから 2 つの Hand オブジェクトをインスタンス化し、Compare メソッドを呼び出してどちらかが勝っているかどうかを判別していることを確認しておいてください。次に図 2 をざっと見てください。このスクリーンショットを見ると、ほんの数分で作成できた軽量 IronPython スクリプトを実行したときのようすがわかります。やはり詳細は後で説明しますが、これを見ると、従来のモジュール テストを実行していることがわかります。つまり、テキスト ファイルからテスト ケース データを読み取り、TwoCardPokerLib モジュールから 2 つの Hand オブジェクトをインスタンス化し、Compare メソッドを呼び出し、実際の結果を予想される結果と比較してテスト ケースの合格/不合格を判断しています。

図 1 コマンド ラインで IronPython を使用したアドホック テスト

>>> import sys
>>> sys.path
['C:\\IronPython', 'C:\\IronPython\\Lib']
>>> sys.path.append(r'C:\ModuleTestingWithPython\TwoCardPokerLib\bin\  Debug')
>>> sys.path
['C:\\IronPython', 'C:\\IronPython\\Lib', 'C:\\ModuleTestingWithPython\\  TwoCardPokerLib\\bin\\Debug']
>>>
>>> import clr
>>> dir()
['_', '__builtins__', '__doc__', '__name__', 'clr', 'site', 'sys']
>>>
>>> clr.AddReferenceToFile("TwoCardPokerLib.dll")
>>> from TwoCardPokerLib import *
>>> dir()
['Card', 'Hand', '_', '__builtins__', '__doc__', '__name__', 'clr',   'site', 'sys']
>>>
>>> c1 = Card()
>>> c2 = Card("9d")
>>> print c1,c2
As 9d
>>>
>>> h1 = Hand(c1,c2)
>>> h2 = Hand("Ah","8c")
>>> expected = 1
>>> actual = h1.Compare(h2)
>>>
>>> if actual == expected: print "Pass\n",
... else: print "Fail\n"
...
Pass
>>>
>>> ^Z

C:\IronPython>

fig02.gif

図 2 IronPython スクリプトを使用したテストの自動化

この後のセクションではテスト対象の TwoCardPokerLib クラス ライブラリについて説明するので、テストの内容はそこで確認できます。それから、図 1 で使用した対話形式の IronPython コマンドについて説明します。次に、図 2 の出力を生成した簡単な Python スクリプトを示して詳細に説明します。テスト ハーネスとテスト対象のライブラリの全ソース コードは、このコラムに付属のダウンロードから入手できます。

最後に、個別のテスト ニーズを満たすために、このコラムで示したアイデアを調整および拡張する方法について簡単に説明します。この記事で説明する Python のテスト技法がソフトウェアのテスト スキル向上に役立つことがおわかりいただけると思います。

テスト対象のモジュール

まず、テスト対象のライブラリを見てみましょう。テスト用の .NET クラス ライブラリを作成することにしました。このライブラリは、運用環境で実際のモジュール テストを実行するときに発生する問題の種類がはっきりわかるのに十分な機能を備えていますが、ライブラリの細部によってテストの問題が見えなくなるほど複雑ではありません。2 枚のカードを使うポーカー ゲームを考えました。このゲームでは、各プレーヤーは、4 組の普通の 52 枚構成のカードから 2 枚だけ受け取ります。自然な結果として、Card クラス、Hand クラス、および Compare メソッドを使用して 2 つの Hand オブジェクトの優劣を決定するオブジェクト指向の設計ができあがります。各 2 枚の手持ちカードは、ストレート フラッシュ (同じマークの連続するカード)、ペア (同じ数の 2 枚のカード)、フラッシュ (同じマークの 2 枚のカード)、ストレート (2 枚の連続するカード)、そして "エース上位" から "4 上位" までに分類することにしました。"3 上位" という手はないことに注意してください。3 と 2 の組み合わせはストレートであり、3 とエースの場合は "エース上位" だからです。このように簡単な例でも、実装しようとするとなかなか興味深いものがあります。実装言語にかかわらず IronPython を使用すると .NET ライブラリを効率よくテストでき、IronPython を使用して従来の COM ライブラリをテストすることもできます。TwoCardPokerLib ライブラリの Card クラスのソース コードを図 3 に示します。

図 3 テスト対象ライブラリの Card クラス

public class Card
{
  private string rank;
  private string suit;
  public Card() {
    this.rank = "A"; // A,2, . . ,9,T,J,Q,K
    this.suit = "s"; // c,d,h,s
  }
  public Card(string s) {
    this.rank = s[0].ToString();
    this.suit = s[1].ToString();
  }
  public override string ToString() {
    return this.rank + this.suit;
  }
  public string Rank {
    get { return this.rank; }
  }
  public string Suit {
    get { return this.suit; }
  }
  public static bool Beats(Card c1, Card c2) {
    if (c1.rank == "A") {
      if (c2.rank != "A") return true;
      else return false;
    }
    else if (c1.rank == "K") {
      if (c2.rank == "A" || c2.rank == "K") return false;
      else return true;
    }
    else if (c1.rank == "Q") {
      if (c2.rank == "A" || c2.rank == "K" || c2.rank == "Q") return false;
      else return true;
    }
    else if (c1.rank == "J") {
      if (c2.rank == "A" || c2.rank == "K" || c2.rank == "Q" || c2.rank == "J")
        return false;
      else
        return true;
    }
    else if (c1.rank == "T") {
      if (c2.rank == "A" || c2.rank == "K" || c2.rank == "Q" ||
        c2.rank == "J" || c2.rank == "T")
        return false;
      else return true;
    }
    else { // c1.rank is 2, 3, . . 9
      int c1Rank = int.Parse(c1.rank);
      int c2Rank = int.Parse(c2.rank);
      return c1Rank > c2Rank;
    }
  } // Beats()

  public static bool Ties(Card c1, Card c2) {
    return (c1.rank == c2.rank);
  }
} // class Card

コードを短くし、中心となるアイデアがはっきりわかるように、運用環境では行わないような省略をいくつかしてあります。たとえば、エラー チェックは省略されています。Card クラスは、意図的に、典型的な .NET モジュールの一般的な機能の多くを含むように設計してあります。既定の Card コンストラクタがあり、スペードのエースのカード オブジェクトを作成していることに注目してください。また、Td のような文字列を受け取って、ダイヤの 10 のカードを表すオブジェクトを作成するコンストラクタがあります。Card オブジェクトの数とマークを公開する get プロパティを実装してあります。そして、一方の Card オブジェクト他の Card オブジェクトに勝っているか、引き分けかを判別する静的メソッドの Beats と Ties が実装されています。Hand クラスのすべてを見てもらおうとすると長すぎるので、クラスの重要な部分についてだけ説明します。Hand クラスには 2 つのメンバ フィールドがあります。

public class Hand {
  private Card card1;
  private Card card2;
  // constructors, methods, etc.
}

重要な前提として、card1 は 2 つの Card オブジェクトのうち大きい方 (または場合によっては同じ値) です。これにより、Hand.Compare メソッドのロジックはずっと簡単になります。Hand クラスには複数のコンストラクタがありますが、これはモジュール テストを実行するときに考慮する必要のある一般的な状況です。既定の Hand コンストラクタは、スペードのエースのカードが 2 枚の手札を作成します。

public Hand() {
  this.card1 = new Card(); 
  this.card2 = new Card();
}

さらに、2 つの Card オブジェクトまたは 2 つの文字列を渡すことのできる 2 つの Hand コンストラクタを実装します。

public Hand(Card c1, Card c2) {
  this.card1 = new Card(c1.Rank + c1.Suit);
  this.card2 = new Card(c2.Rank + c2.Suit);
}
public Hand(string s1, string s2) {
  this.card1 = new Card(s1);
  this.card2 = new Card(s2);
}

図 4 では、4 つのプライベート ヘルパ メソッドを実装しています。見るとわかるように、プライベート メソッドは IronPython のテスト スクリプトには公開されていません。

図 4 プライベート メソッド

private bool IsPair() { return this.card1.Rank == this.card2.Rank; }
private bool IsFlush() { return this.card1.Suit == this.card2.Suit; }
private bool IsStraight() {
  if (this.card1.Rank == "A" && this.card2.Rank == "K") return true;
  else if (this.card1.Rank == "K" && this.card2.Rank == "Q") return true;
  // etc: Q-J, J-T, T-9, 9-8, 8-7, 7-6, 6-5, 5-4, 4-3, 3-2
  else if (this.card1.Rank == "A" && this.card2.Rank == "2") return true; 
  else return false;
}
private bool IsStraightFlush() { return this.IsStraight() &&
  this.IsFlush();
}

一般に、モジュール テストを行うときは、プライベート メソッドは明示的にはテストしません。これは、ヘルパ メソッドのエラーは、そのヘルパを使用するパブリック メソッドをテストすると明らかになる、という考えによるものです。Hand.Compare メソッドは驚くほど巧妙です。Beats および Ties というプライベート ヘルパ メソッドを作成し、それを使用してパブリック メソッドである Compare を実装しています。

public int Compare(Hand h) {
  if (this.Beats(h))
    return 1;
  else if (this.Ties(h))
    return 0;
  else if (h.Beats(this))
    return -1;
  else
    throw new Exception("Illegal path in Compare()");
}

Hand.Compare には、古い C 言語の strcmp(s,t) 関数のパラダイムを使用しています。"左側" の Hand パラメータ ("this" オブジェクト) が "右側" のパラメータ (明示的な Hand 入力パラメータ) に勝つ場合、Compare は 1 を返します。右のパラメータが左のパラメータに勝つ場合、Compare は -1 を返します。このゲームのルールに従って 2 つの Hand オブジェクトが等しい場合、Compare は 0 を返します。ほとんどの処理はプライベート メソッドの Beats で行われています。これを図 5 に示します。

図 5 Beats メソッド

private bool Beats(Hand h) {
  if (this.IsStraightFlush()) {
    if (!h.IsStraightFlush()) return true;
    if (h.IsStraightFlush()) {
      if (Card.Beats(this.card1, h.card1)) return true;
      else return false;
    }
  } // this.IsStraightFlush()
  else if (this.IsPair())
    // code  
  else if (this.IsFlush())
   //  code
  else if (this.IsStraight())
    // code    
  else 
    // code for Ace-high down to Four-high
  }

  return false;
}

Beats のコードは約 1 ページの長さがあります。興味がある場合は、付属のダウンロード コードで詳細を調べてみてください。Ties プライベート メソッドには意図的にロジック エラーを組み込んであります。

else if (this.IsFlush() && h.IsFlush() &&  
  this.card1.Rank == h.card1.Rank)          // error
    return true;

比較対象の 2 つの Hand オブジェクトがどちらもフラッシュの場合、上位カードの数が同じかどうかを調べます。しかし、本来は行う必要のある 2 番目のカードの検査を行っていません。

else if (this.IsFlush() && h.IsFlush() &&  
  this.card1.Rank == h.card1.Rank &&
  this.card2.Rank == h.card2.Rank)         // correct
    return true;

このロジック エラーにより、図 2 に示すようなテスト ケース不合格が発生します。ライブラリの作成には Visual Studio を使用し、C:\Module TestingWithPython に TwoCardPokerLib という名前のクラス ライブラリ プロジェクトを作成しました。その結果、C:\ModuleTestingWithPython\TwoCardPokerLib\bin\Debug に TwoCardPokerLib.dll ファイルがあります。

アドホックな対話型モジュール テスト

それでは、IronPython を使用して .NET ライブラリを調査およびテストする方法を見ていきましょう。特に、図 1 のスクリーンショットで示されている各コマンドに注目します。図 1 に示した出力の最初の部分では、Version 1.1.1 の IronPython を使用していることが示されています。

IronPython は、Microsoft がスポンサーになっているオープン ソース プロジェクトである CodePlex (codeplex.com/IronPython) から無料でダウンロードできます。.NET Framework 2.0 が必要であり、このバージョンの Framework をサポートするマシンで動作します。ここでは Windows Vista を使用しています。実際には、IronPython はインストールしません。1 つの zip ファイルをマシンにダウンロードして、その内容を適当なディレクトリに解凍するだけです。この例では、IronPython のすべてのファイルとサブディレクトリを C:\IronPython に格納しました。Python のコマンド ライン インタプリタ (ipy.exe) を起動し、オプションの -X:TabCompletion 引数を指定すると、タブ補完機能が有効になります。「ipy.exe -h」と入力すると、全オプションの一覧が表示されます。IronPython は起動時に、site.py という名前の特殊な起動スクリプトが存在すると、それを実行します。ここでは、IronPython に付属する標準の site.py ファイルを、何も変更しないで使用しました。

IronPython の初期化が済んだ後、IronPython の現在のシステム パスの情報を確認します。

>>> import sys
>>> sys.path
['C:\\IronPython', 'C:\\IronPython\\Lib']

最初に import sys コマンドを発行し、特別な IronPython sys モジュールに含まれるメソッドにアクセスできるようにします。次に、現在のディレクトリに存在しないファイルを検索するときに IronPython が調べるパスの一覧を表示します。これは、Windows オペレーティング システムの Path 環境変数と似たところがある、IronPython のローカルなメカニズムと考えることができます。TabCompletion 機能を有効にして IronPython を起動したので、sys モジュールから使用できるプロパティとメソッドを知りたいときには、「sys.」と入力してから Tab キーを繰り返し押します。次に、テスト対象の .NET モジュールがある場所を IronPython に指示します。

>>> sys.path.append(r'C:\ModuleTestingWithPython\TwoCardPokerLib\bin\Debug')
>>> sys.path
['C:\\IronPython', 'C:\\IronPython\\Lib', 'C:\\ModuleTestingWithPython\\TwoCardPokerLib\\bin\\Debug']

sys モジュールの path.append メソッドを使用して、新しいディレクトリを IronPython の検索リストに追加します。追加する文字列引数の前にある r という文字に注意してください。Python では、文字列の前後に単一引用符または二重引用符を使用できます。ただし、一部の言語とは異なり、Python では単一引用符と二重引用符のどちらの文字列でも、内部にある \n のようなエスケープ文字が評価されます。文字列がリテラル (Python の用語では "raw" 文字列) として厳密に解釈されるようにするには、前の例のように r 修飾子を使用します。新しいディレクトリを追加した後、sys.path コマンドを発行して入力ミスがないことを確認します。次に、まず .NET モジュールを読み込むことができるメソッドを有効にすることで、テスト対象の DLL を読み込む準備をします。

>>> import clr
>>> dir()
['_', '__builtins__', '__doc__', '__name__', 'clr', 'site', 'sys']

import clr (common language runtime (共通言語ランタイム) の略) コマンドを発行すると、clr モジュールにアクセスできるようになり、このモジュールには .NET アセンブリの読み込みに使用できる複数のメソッドが含まれます。次に、dir() という Python コマンドを使用して、現在アクセスできるモジュールを確認します。現時点では TwoCardPokerLib ライブラリにアクセスできないことに注意してください。そこで、clr モジュールを使用して、テスト対象のライブラリにアクセスできるようにします。

>>> clr.AddReferenceToFile("TwoCardPokerLib.dll")
>>> from TwoCardPokerLib import *
>>> dir()
['Card', 'Hand', '_', '__builtins__', '__doc__', '__name__', 'clr', 'site', 'sys']

AddReferenceToFile メソッドを使用し、現在の IronPython 環境が TwoCardPokerLib DLL を参照できるようにします。Python は大文字と小文字を区別するので、たとえば 「addreferencetofile」と入力すると、そのような属性 (メソッド) はないというエラーが発生します。

テスト対象のメソッドへの参照を追加した後、import ステートメントを使用して、モジュールを使用できるようにする必要があります。「import TwoCardPokerLib」と入力することもできますが、実際には「from TwoCardPokerLib import *」と入力します。最初に示した簡単な形式を使用すると、モジュール内のすべてのものを完全に修飾する必要があります。たとえば、単に「Hand」と入力するのではなく、「TwoCardPokerLb.Hand」と入力する必要があります。from-<モジュール>-import-<クラス> の形式の import コマンドを使用すると、親モジュール名を省略して入力できます。dir() コマンドを発行すると、TwoCardPokerLib モジュールに含まれる Card および Hand クラスを使用できるようになったことがわかります。2 つの Card オブジェクトを作成して、テスト対象のモジュールを検査できます。

>>> c1 = Card()
>>> c2 = Card("9d")
>>> print c1,c2
As 9d

既定の Card コンストラクタを使用して、c1 という名前のオブジェクトをインスタンス化します。前のセクションで説明したように、既定の Card コンストラクタはスペードのエースのオブジェクトを作成することを思い出してください。次に、既定ではない Card コンストラクタを使用して、ダイヤの 9 を表すオブジェクトを作成します。一部の言語とは異なり、"new" のようなキーワードを使用しなくてもオブジェクトをインスタンス化できることに注目してください。前の手順で "import TwoCardPokerLib" という短いステートメントを使用した場合は、"c1 = TwoCardPokerLib.Card()" のようにコンストラクタを呼び出します。組み込みの Python print ステートメントを使用して、2 つの Card オブジェクトを表示します。実際には、IronPython print ステートメントはクラス ライブラリで実装した Card.ToString() メソッドを呼び出します。2 つの Hand オブジェクトをインスタンス化したので、ここで Hand.Compare メソッドを呼び出します。

>>> h1 = Hand(c1,c2)
>>> h2 = Hand("Ah","8c")
>>> expected = 1
>>> actual = h1.Compare(h2)  

作成した 2 つの Card オブジェクトを Hand コンストラクタに渡して 1 番目の手札を作成し、次にコンストラクタの文字列パラメータ バージョンを使用して 2 番目の手札をインスタンス化します。エースと 9 の手札の方がエースと 8 の手札より強いので、Compare メソッドは 1 を返すものと予想されるので、この値を expected という名前の変数に格納します。Python は動的な型指定の言語なので、データ型を宣言する必要はありません。Hand.Compare メソッドを呼び出して、結果を actual という名前の変数に格納します。コマンド ラインで「Hand.」と入力して Tab キーを押すと、Hand クラスで使用できるメソッドが表示されます。Compare や ToString のようなパブリック メソッドは表示されますが、Beats や Ties のようなプライベート メソッドは表示されません。アドホックな対話形式テストの合格/不合格の結果は、次のようにして確認できます。

>>> if actual == expected: print "Pass\n",
... else: print "Fail\n"
...
Pass
>>>

コマンド ラインでの Python の if-then 構文は普通と少し異なるので、次のセクションで説明します。ただし、基本的には、actual という名前の変数の値が expected という名前の変数の値と等しいかどうかを調べます。等しい場合は "Pass" というメッセージを表示し、等しくない場合は "Fail" と表示します。各文字列に改行文字を含めていることに注意してください。途中には "..." という IronPython インタープリタからの応答は、コマンドが不完全であり、インタープリタがコマンドの完了を待っていることを示します。

軽量モジュール テストの自動化

ここでは、軽量の IronPython スクリプトを作成して .NET のクラス ライブラリをテストする方法を説明します。図 6 のスクリプトが、図 2 の出力を生成しました。

図 6 harness.py ファイル

# harness.py
# test TwoCardPokerLib.dll using data in TestCases.txt

print "\nBegin test run\n"
import sys
print "Adding location of TwoCardPokerLib.dll to sys.path"
sys.path.append(r'C:\ModuleTestingWithPython\TwoCardPokerLib\bin\Debug')

import clr
print "Loading TwoCardPokerLib.dll\n"
clr.AddReferenceToFile("TwoCardPokerLib.dll")
from TwoCardPokerLib import *

print "=================================="
fin = open("TestCases.txt", "r")
for line in fin:
  if line.startswith("$"):
    continue
  (caseID,input,method,expected,comment) = line.split(':')
  expected = int(expected)
  (left,right) = input.split(',')
  h1 = Hand(left[0:2],left[2:4])
  h2 = Hand(right[0:2],right[2:4])
  print "Case ID = " + caseID
  print "Hand1 is " + h1.ToString() + "   " + "Hand2 is " + h2.ToString()
  print "Method = " + method + "()"
  actual = h1.Compare(h2)

  print "Expected = " + str(expected) + " " + "Actual = " + str(actual)
  if actual == expected:
    print "Pass"
  else:
    print "** FAIL **"


  print "=================================="

fin.close()
print "\nEnd test run"
# end script

この IronPython テスト ハーネス スクリプトの先頭にはコメントがあります。

# harness.py
# test TwoCardPokerLib.dll using data in TestCases.txt

# は、Python スクリプトのコメント文字です。Python は行ベースであるため、コメントは行の終わりまで続きます。また、Python では、3 つの単一引用符を使用して複数行のコメントを作成できます。Python スクリプトのファイル拡張子は .py で、メモ帳などの任意のテキスト エディタを使用して作成できます。テスト ハーネスは、テスト ケース データを TestCases.txt という外部ファイルから読み取ります。

0001:AcKc,AdAs:Compare:1:  "royal flush" > pair Aces
0002:Td9d,Th9h:Compare:0:  straight flush diamonds == straight flush hearts
$0003:Ah2c,As9c:Compare:1:   straight > Ace high
0004:9h6h,9d7d:Compare:-1:  flush 9-6 high < flush 9-7
0005:KcJh,As5d:Compare:-1: King high < Ace high

この例のテスト ケースは 5 個だけですが、運用シナリオでは何百または何千にもなります。Hand.Compare に対する 7,311,616 (または 524) という有効な入力がありますが、これは簡単なモジュールであっても網羅的なテストの実行が実用的でないことを示しています。

各行は 1 つのテスト ケースを示します。各フィールドはコロンで区切ります。最初のフィールドはテスト ケース ID です。2 番目のフィールドは、コンマで区切られた、2 つの Hand オブジェクトに対する情報を含む文字列です。3 番目のフィールドはテストするメソッドを示します。4 番目のフィールドは予想される値です。5 番目のフィールドは省略可能で、テスト ケースのコメントです。テスト ケース 0003 の前に $ という文字があることに注意してください。テスト ハーネスでは、この文字を検索し、該当するテスト ケースをスキップします。テスト ケース 0004 は、テスト対象の Hand.Compare メソッドによって呼び出される Hand.Ties に意図的に組み込まれている (例示のみを目的とする) ロジック エラーのために、結果は不合格になります。テスト ケース データはテキスト ファイルに簡単に保存できます。また、テスト ケース データをハーネスに直接埋め込んだり、XML ファイルにケースを格納したりすることもできます。Python は、任意のテスト ケース格納方法を簡単に処理できます。次に、コマンド シェルにメッセージを表示します。

print "\nBegin test run\n"

Python の文字列は二重引用符または単一引用符のどちらでも区切ることができ、どちらの引用スタイルでも内部のエスケープ文字が評価されるので、どちらの引用符を使用するかは好みの問題です。私はたいてい二重引用符を使用しますが、r 修飾子を使用して文字列をリテラルにするときは単一引用符をよく使います。次に、Python テスト ハーネス スクリプトは、テスト対象の DLL の場所を Python のシステム パスに追加します。

import sys
print "Adding location of TwoCardPokerLib.dll to sys.path"
sys.path.append(r'C:\ModuleTestingWithPython\TwoCardPokerLib\bin\Debug')

ここでは、テスト対象の DLL の場所をハードコーディングしています。代わりに、コマンド ライン引数を Python スクリプトに渡し、組み込みの sys.argv 配列でアクセスすることもできます。たとえば、次のようにスクリプトを呼び出すものとします。

  > ipy.exe   harness.py   C:\Data   MyLib.dll

このようにすると、sys.argv[0] はスクリプトの名前 (harness.py)、sys.argv[1] は 1 番目の引数 (C:\Data)、sys.argv[2] は 2 番目の引数 (MyLib.dll) をそれぞれ保持します。次に、テスト対象のモジュールを読み込むようにハーネスに指示します。

import clr
print "Loading TwoCardPokerLib.dll\n"
clr.AddReferenceToFile("TwoCardPokerLib.dll")
from TwoCardPokerLib import *

IronPython を使用して .NET ベースのライブラリを読み込むには複数の方法があります。clr.AddReferenceToFile メソッドは、追加するライブラリの場所が sys.path に含まれている場合は、簡単で効果的な方法です。ただし、clr モジュールには、clr.AddReference、clr.AddReferenceByName、clr.AddReferenceByPartialName、clr.AddReferenceToFileAndPath などの代わりのメソッドも含まれます。ここでは * のワイルドカードを使用してすべてのクラスを TwoCardPokerLib ライブラリから読み込んでいますが、名前を指定して特定のクラスを読み込むこともできます。Python には、テキスト ファイルを 1 行ずつ処理する方法が複数用意されています。この例では、組み込みのファイル オープン関数を使用し、テスト ケース ファイルの名前と r 引数を渡して、読み取り用にファイルを開くことを示しています。

print "=================================="
fin = open("TestCases.txt", "r")
for line in fin:
  # process line here

他のよく使われるモードとしては、書き込みの w や、追加の a などがあります。モード引数は省略可能であり、既定値は r なので、引数を指定しなくてもかまいません。Python 言語の機能、構文、および組み込み関数についてのわかりやすいリファレンスが、docs.python.org にあります。open 関数からはファイル ハンドラが返されるので、これを fin という名前の変数に代入しています。その後、for ループを使用して 1 行ずつ処理します。行を格納する変数は "line" という名前にしていますが、任意の有効な変数名を使用できます。Python の他の言語とは違う構文に注意してください。ほとんどの言語で {. .. } や begin. .. end のような開始 - 終了トークンが使用されているのに対し、Python では、コロンとインデントの組み合わせで、ステートメント ブロックの開始と終了を示します。メインの処理ループの中では、組み込みの startswith 文字列関数を使用して、テスト ケース ファイルから読み取った現在の行が、ドル記号で始まっているかどうかを調べています。

if line.startswith("$"):
  continue

$ で始まっている場合は、continue ステートメントを使用してループの残りのステートメントをスキップし、テスト ケース ファイルから次の行を読み取ります。Python は、ループと決定の完全な制御構造を備えています。次に、テスト ケース データの行を個別のフィールドで解析します。

(caseID,input,method,expected,comment) = line.split(':')
expected = int(expected)

Python のタプルと呼ばれる優れた機能と組み込みの split 文字列関数を使用して、データを簡単に解析できます。次に示す従来の配列の方法を使用してもこのタスクを実行できますが、筆者は Python のタプルを使用する方が短くて理解しやすいコードになると考えます。

tokens = line.split(':')
caseID = tokens[0]
input = tokens[1]
method = tokens[2]
expected = tokens[3]
comment = tokens[4]

int() 関数を使用して expected という名前の変数を string 型から int 型に明示的に変換し、expected と actual を比較できるようにします。便利な型変換関数としては、他に str()、float()、long()、bool() などがあります。次に、解析操作をもう 1 回行って、2 つの入力 Hand を抽出します。

(left,right) = input.split(',')
h1 = Hand(left[0:2],left[2:4])
h2 = Hand(right[0:2],right[2:4])

split を使用した最初の解析の後で、変数 input は KcJh,As5d のような文字列値を保持しています。そこで、引数を指定して split を再度呼び出し、2 つの結果を left および right という名前の変数に格納します。文字列変数の内容は、left が KcJh で right が As5d です。他の多くの言語とは異なり、Python には部分文字列メソッドはありません。代わりに、Python では配列のインデックスを使用して部分文字列を抽出します。配列のインデックスは 0 から開始するので、変数 left が KcJh を保持している場合は、式 left[0:2] を使用すると Kc が得られ、式 left[2:4] を使用すると Jh が得られます。大きい文字列から部分文字列を取得するには、(予想したとおりに) 大きい文字列での最初の文字のインデックスと、最後の文字のインデックスより 1 だけ大きい値を指定することに注意してください。個別のカードを表す文字列を取得した後は、それを Hand コンストラクタに渡します。次に、入力データをシェルにエコーします。

print "Case ID = " + caseID
print "Hand1 is " + h1.ToString() + "   " + "Hand2 is " + h2.ToString()
print "Method = " + method + "()"

Python では文字 + を使用して文字列を連結するので、前の 3 つの print ステートメントでは 3 つの文字列を印刷しています。print 関数は , 文字で区切られた複数の引数を受け付けるので、次のようなステートメントを使用しても同じ出力を生成できます。

print "Case ID = " , caseID

これで、テスト対象のメソッドを呼び出すことができるようになりました。

actual = h1.Compare(h2)
print "Expected = " + str(expected) + " " + "Actual = " + str(actual)

Hand.Compare メソッドをインスタンス メソッドとして実装したので、h1 Hand オブジェクトのコンテキストから呼び出します。静的メソッドとして Compare をコーディングしたとすると、次のように呼び出します。

actual = Compare(h1,h2)

この時点では、actual は int を戻すように定義されている Compare メソッドからの戻り値で、expected は明示的に int にキャストしたので、変数 actual と expected はどちらも int 型であることに注意してください。そのため、単一の出力文字列に連結できるよう、actual と expected を文字列にキャストします。そして、テスト ケースの合格/不合格の結果を表示します。

if actual == expected:
  print "Pass"
else:
  print "** FAIL **"

print "=================================="

Python では、インデントを使用してブロック ステートメントの開始と終了を示します。他の言語でのプログラミング経験が豊富な場合、Python でインデントを使用することに最初はとまどうかもしれません。しかし、私が話したことのあるほとんどのテスト担当者は、Python の独特な構文の問題にはすぐに慣れると言っています。

合格したテスト ケースの総数を追跡する場合は、次のようにメインの処理ループの外側でカウンタを初期化します。

numPass = numFail = 0

そして、if-then 構造を次のように変更します。

if actual == expected:
  numPass += 1
  print "Pass"
else:
  numFail += 1
  print "** FAIL **"

それから、結果を処理ループの外側で出力します。

print "Num pass = " , numPass ,  " num fail = " ,  numFail

Python では、int 変数をインクリメントする numPass++ や ++numPass のような構文はサポートされていません。ここでは単純に結果をコマンド シェルに出力していますが、テキスト ファイル、XML ファイル、SQL データベース、その他のストレージに結果を書き込むことも簡単にできます。これでテスト ハーネスは終了です。

fin.close()
print "\nEnd test run"
# end script

テスト ケース ファイルの参照を閉じてファイル ハンドルを解放し、テストが完了したことを示すメッセージを出力します。この例のスクリプト構造は非常に単純ですが、Python には複雑なスクリプトを管理できる機能が用意されています。たとえば、運用環境では、間違いなくスクリプト全体を例外ハンドラの中にラップします。

try
  # harness code here
except
  # handle any exceptions here 

さらに、Python はプログラマが定義する関数をサポートするので、ハーネスをメイン関数とヘルプ関数というように構造化できます。

まとめ

モジュール テストの自動化は、おそらく、ソフトウェア テストの自動化の中でも最も基本的なものでしょう。私は、プログラミング言語がテストの自動化に適しているかどうかを評価するときには常に、その言語でモジュール テストをどれだけうまく処理できるかを調べます。私の考えでは、IronPython はこの基準に何の問題もなく合格します。すべてのテスト タスクとシナリオに完璧なプログラミング言語はありません。つまり、IronPython は、モジュール テスト言語として優れた選択肢になるような多くの機能を備えています。

最後に、IronPython での軽量モジュール テストの自動化が単体テストとどのように関係するのかを説明します。ほとんどの場合、NUnit などのフレームワークによる単体テストは、モジュール コードの内部に直接配置されます。単体テストによるテスト駆動型の開発は、主として開発者の作業です。開発の間に単体テスト手法を使用したとしても、それだけを目的とする徹底的なモジュール テストを行う責任を免除されることはありません。そのような場合に Python を使用できるのです。つまり、Python でのモジュール テストは、単体テストに代わるものではなく、単体テストを補完するのです。2 つの方法を併用することにより、より高品質で信頼性の高いソフトウェアを開発できます。

ご意見やご質問は、James (testrun@microsoft.com) まで英語でお送りください。

Dr. James McCaffrey は、Volt Information Sciences, Inc. で、マイクロソフトのソフトウェア エンジニア向けの技術トレーニングを担当しています。これまでに、Internet Explorer、MSN サーチなどの複数のマイクロソフト製品にも携わってきました。また、『.NET Test Automation Recipes』の著者でもあります。連絡先は、jmccaffrey@volt.com または v-jammc@microsoft.com です。