SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

C#で始めるテスト駆動開発入門

TDDでデータベースと付き合う方法

C#で始めるテスト駆動開発入門(7)

ダウンロード CsTdd07.zip (474.5 KB)

データベースの内容を安定させる

 ここまで書いてきたユニットテストは、実際にデータベースにアクセスするものでした。ですから、Customersテーブルの例えば顧客名「Thomas Hardy」が違う名前に変更されると、テストが失敗するようになります。

データベースエクスプローラーでデータを書き換える

 ビジネスロジックの大部分のユニットテストでは、データベースに実際にアクセスする必要がないので、モックやダミー実装に置き換えられるようにすればよいです。しかし、例えば今回のDbCommandHelperのように、データベースへのアクセスを実際にテストしなければならない部分は、どうしたらよいでしょう。

テスト用のデータで初期化する

 まず考え付くのは、テストの前にデータベースの内容を書き換えて、いつも同じにすることでしょう。バッチ処理で行ったり、データ量がそれほどでもなければテストの中で実行したり、乱暴ですが場合によってはデータベースのファイル自体を置き換えてしまうことも可能でしょう。

 それほどデータ量が多くない場合、ADO.NETでは次のようにして、簡単にデータをXMLファイルに落とすことができます。

テストコード: テーブルの全データをXMLファイルに保存する
[TestFixture]
public class テスト用データ {

  string xmlFileName = "テスト用顧客データ.xml";
  string cmdText = @"SELECT * FROM Customers";

  private DataTable GetAllCustomersForTest() {
    using (DbCommandHelper改5 helper = new DbCommandHelper改5())
    using (DbCommand cmd = helper.CreateCommand(cmdText))
    using (SqlCeDataAdapter adp = new SqlCeDataAdapter((SqlCeCommand)cmd)) {
      var tbl = new DataTable("Customers");
      adp.FillSchema(tbl, SchemaType.Source);
      adp.Fill(tbl);
      return tbl;
    }
  }

  //[TestCase]  //←普段はコメントアウトしておくこと
  public void CustomersテーブルをXMLファイルに落とす() {
    var tbl = GetAllCustomersForTest();
    tbl.WriteXml(xmlFileName, XmlWriteMode.WriteSchema);

    Assert.Inconclusive("これはテストデータを作成する。普段は使わない");
  }
}

 こうやって用意しておいたXMLファイルのデータを、テスト実行前にデータベースへ投入してやります。DROP-CREATEしてもよいですが、ここでは既存データとマージする方法を示します。

テストコード: XMLファイルからデータベースにデータを投入する
public int LoadTestData() {
  var tableFromFile = new DataTable();
  tableFromFile.ReadXml(xmlFileName);

  var dbTable = GetAllCustomersForTest();
  dbTable.Merge(tableFromFile);

  using (TransactionScope tran = new TransactionScope()) {
    int affected = 0;
    using (SqlCeDataAdapter adp = new SqlCeDataAdapter(cmdText, DbConnectionProvider改5.Connection))
    using (SqlCeCommandBuilder cb = new SqlCeCommandBuilder(adp))
      affected = adp.Update(dbTable);

    tran.Complete();
    return affected;
  }
}

 それぞれのユニットテストを実行する前に、毎回このコードを呼び出してデータを投入しましょうか? そうすれば、たしかに安定してテストできるようにはなります。が、テストの実行時間がとても長くなってしまいます。それでは実用になりません。

System.Transactionsを使う

 System.Transactions名前空間を使うと、多重コミットがサポートされます。ユニットテストのコードでトランザクションを開始してから製品コードを呼び出すと、製品コード側でデータベースに変更を加えてコミットしても、その後でユニットテスト側でロールバックしてしまえば、データベースにはまったく変更が残りません。

System.Transactionsを使ってデータベースの内容を安定させる
System.Transactionsを使ってデータベースの内容を安定させる

 データの投入は、例えばTestFixtureSetUpで行います。

テストコード: テストクラスの単位で、最初にデータを投入する
[TestFixture]
public class 顧客管理ロジック改5Test {

  [TestFixtureSetUp]
  public void TestFixtureSetUp() {
    テスト用データ.LoadTestData(); //データ投入(時間が掛かる!)
  }

  // 以下略

 このとき、LoadTestData()の中でスタティック変数にフラグを立てておくと、NUnitランナーではテストをリロードするまでクリアされません。テストをリロードするまで、長時間掛かるデータのロードを再実行しなくても済みます。

 そして、データベースに影響を与えるテストは、TransactionScopeで囲ってロールバックするようにします。例えば、さきほどのLoadTestData()はメソッド内でTransactionScopeを使ってコミットしていますが、次のようなテストが書けます。

テストコード: 製品コード側のコミットを取り消す
[TestCase]
public void LoadTestDataTest() {

  // 準備: 該当するデータがあったら、手動で変えておいてください。

  // ↓データベースを書き換えてあるので、ヒットしないはず
  Assert.AreEqual(0, 顧客管理ロジック改5.前方一致で姓名を検索する("Trujillo").Count());

  using (TransactionScope tran = new TransactionScope(TransactionScopeOption.RequiresNew)) {

    LoadTestData();  // LoadTestData() の中で、トランザクションをコミットした

    // ファイルからロードしたデータになっているので、こんどは1件ヒットする
    Assert.AreEqual(1, 顧客管理ロジック改5.前方一致で姓名を検索する("Trujillo").Count());

  } // こちら側のトランザクションはロールバック

  // ロールバックされている
  Assert.AreEqual(0, 顧客管理ロジック改5.前方一致で姓名を検索する("Trujillo").Count());

  DbConnectionProvider改5.CloseTransactionConnection(); //トランザクション中で使った接続をクローズ
}

System.Transactionsを使うときの注意

 このように非常に便利なSystem.Transactionsですが、「トランザクションの昇格」に注意する必要があります。

  • トランザクションの昇格が発生すると、MS DTC(分散トランザクションコーディネーター)が必要になる。1つのTransactionScopeに複数のDB接続を参加させると、昇格が発生する。昇格が発生するとパフォーマンスが落ちる。
  • SQL Server 2005では、接続を閉じて開くだけで(同時に開いているのは1本でも)、昇格が発生する。
  • SQL Server 2008以降は、同時に複数接続を開こうとしなければ大丈夫(参考記事)。
  • SQL Server Compactは分散トランザクションに対応していない。トランザクション内で1本の接続を使いまわす必要がある。

 サンプルコードはSQL Server Compactを使っています。そのため、上のコードで使っているDbConnectionProviderは、トランザクション中は同じ接続オブジェクトを使い、トランザクションが全部終わってから接続を開放するように改造してあります(サンプルコードのCsTdd07最終形プロジェクトを参照)。

まとめ

  • テストしにくいものは切り離せ! 実際にDBに接続してテストしなければならないコードを、できるだけ少なくしよう。
  • DBの挙動をテストしなければならないときは、DBの内容を安定させる手法を考えよう。
  • テスト目的がDBの挙動でないところは、DBアクセス部分をモックやダミーで置き換えると、テスト時間が短縮できる。

 なお、記事中では紹介しきれなかったモックを使ったテストなども、サンプルコードCsTdd07最終形プロジェクトに収めてありますので、参考にしてください。データベースにアクセスするコードは長くて汚くなりがちですが、このようにしてTDDしていくとコードメトリックスも良好になるものです(次の図)。

【参考】Visual Studio 2012 Ultimateで、CsTdd07最終形プロジェクトの各種メトリックスを見る
【参考】Visual Studio 2012 Ultimateで、CsTdd07最終形プロジェクトの各種メトリックスを見る

この記事は参考になりましたか?

連載通知を行うには会員登録(無料)が必要です。
既に会員の方はを行ってください。
C#で始めるテスト駆動開発入門連載記事一覧

もっと読む

この記事の著者

biac(ばいあっく)

HONDA R&Dで自動車の設計をやっていた機械屋さんが、技術の進化スピードに魅かれてプログラマーに。以来30年ほど、より良いコードをどうやったら作れるか、模索の人生。わんくま同盟の勉強会(名古屋)で、よく喋ってたりする。2014/10~2019/6 Microsoft MVP (Windows Devel...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/6783 2012/10/30 14:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー