はじめに
データベースを読み書きする部分のユニットテストがやりにくいのには、いくつか理由があります。
- 複数人でテストを同時に実行すると、競合する
- データベースを使ったテストは、時間が掛かる
- データベース内のデータが変わると、テストが失敗する
1番目は、各自の開発環境にテスト用のデータベースを用意することで、解決できます。2番目の問題は、データベースにアクセスするコードをロジックから分離して、データベースに実際にアクセスするテストケースを減らすことで、改善できます(ロジックのテストにはモックやダミーを使います)。3番目は、テストのたびにデータベースの内容を初期化することが基本になりますが、そうするとテストに長い時間が掛かるようになってしまいます。
今回は、ビジネスロジックの開発時にモックやダミーを使いやすくするにはどうするか、また、テスト時にデータベースの内容を安定させるにはどうしたらよいかを、考えてみます。
対象読者
- TDDに興味をお持ちの.NET Frameworkの開発者。
必要な環境
サンプルコードを試してみるには、C# 2010(Expressで可)またはVisual Studio 2012(Expressで可)、およびNUnit 2.6とSQL Server Compact 3.5が必要です。本稿執筆時点では、下記から入手できます。
-
C# 2010 Express: Microsoft Visual Studio Express
Visual Studio Express 2012: Visual Studio Express 2012 for Windows Desktop -
NUnit 2.6: NUnit V2 2.6.1
NUnitのインストール手順: NUnit 2.5 の導入 Step by Step(筆者サイト、旧バージョンでの説明ですが基本的に同じです) - SQL Server Compact 3.5: Microsoft SQL Server Compact 3.5 Service Pack 2
今回はSQL Serverが必要です。無償のExpressやCompact 4.0等で構いませんが、Northwindデータベースを使います。なお、サンプルコードではSQL Server Compact 3.5 SP2を使っていますので、接続文字列やクラス名などは適宜読み替えてください。
とりあえずコードを作ってみる
それでは、Customersテーブルから複数件のデータを取得してくるコードを、とりあえず書いてみましょう。今回は、ADO.NETを使うことにします。
製品のロジックの中に、こんなメソッドを作ります。
| クラス名.メソッド名 | 顧客管理ロジック.前方一致で姓名を検索する()。 |
|---|---|
| 引数 | string型。 |
| 返値 | 顧客情報クラスのコレクション型。Customersテーブルから、顧客のファーストネームまたはラストネームと引数を前方一致で比較して、一致したデータを返す。 |
Nothwindデータベースが初期状態のままなら、次のようなユニットテストを書くことができます。
[TestFixture]
public class 顧客管理ロジックTest {
[TestCase] //製品コード:空のリストを返す。Customerクラスは宣言だけ。
public void 前方一致で姓名を検索するTest_引数が空なら0件() {
var head = "";
IList<Customer> customers = 顧客管理ロジック.前方一致で姓名を検索する(head);
Assert.AreEqual(0, customers.Count());
}
[TestCase] //製品コード:CustomerクラスとSQL文(パラメーター未使用)を実装
public void 前方一致で姓名を検索するTest_ファーストネームが一致() {
var head = "Th";
var customers = 顧客管理ロジック.前方一致で姓名を検索する(head);
var first = customers[0];
Assert.AreEqual("UK", first.Country);
Assert.AreEqual("Around the Horn", first.CompanyName);
Assert.AreEqual("AROUT", first.CustomerID);
Assert.AreEqual("Thomas Hardy", first.ContactName);
}
[TestCase] //製品コード:SQL文を完成させ、パラメタライズドクエリーに修正
public void 前方一致で姓名を検索するTest_ラストネームが一致() {
var head = "An";
var customers = 顧客管理ロジック.前方一致で姓名を検索する(head);
var first = customers[0];
Assert.AreEqual("Germany", first.Country);
Assert.AreEqual("Alfreds Futterkiste", first.CompanyName);
Assert.AreEqual("ALFKI", first.CustomerID);
Assert.AreEqual("Maria Anders", first.ContactName);
}
}
このテストを通すように、製品コードを書きます(実際は、テストケースを1つずつ進めていきます)。
public class Customer {
public string Country { get; set; }
public string CompanyName { get; set; }
public string CustomerID { get; set; }
public string ContactName { get; set; }
}
public class 顧客管理ロジック {
public static IList<Customer> 前方一致で姓名を検索する(string head) {
var list = new List<Customer>();
if(string.IsNullOrWhiteSpace(head))
return list; //引数が空文字のときは、空のリストを返す。
// DB アクセス
using(SqlCeConnection conn = new SqlCeConnection(@"Data Source=C:\Program Files (x86)\Microsoft SQL Server Compact Edition\v3.5\Samples\Northwind.sdf")){
conn.Open();
var cmdText =
@"SELECT Country, [Company Name], [Customer ID], [Contact Name]
FROM Customers
WHERE ([Contact Name] LIKE @p1) OR ([Contact Name] LIKE @p2)";
using (DbCommand cmd = new SqlCeCommand(cmdText, conn)) {
{
var p1 = cmd.CreateParameter();
p1.ParameterName = "@p1";
p1.DbType = System.Data.DbType.String;
p1.Value = head + "%"; //ファーストネームとの前方一致
cmd.Parameters.Add(p1);
} //(変数p1のスコープをここで切っている)
{
var p2 = cmd.CreateParameter();
p2.ParameterName = "@p2";
p2.DbType = System.Data.DbType.String;
p2.Value = "% " + head + "%"; //ラストネーム・ミドルネームとの前方一致
cmd.Parameters.Add(p2);
}
using (DbDataReader reader = cmd.ExecuteReader()) { //クエリー実行
while (reader.Read()) { //結果を一行ずつオブジェクトに詰め替え
var customer = new Customer() {
Country = reader.GetString(0),
CompanyName = reader.GetString(1),
CustomerID = reader.GetString(2),
ContactName = reader.GetString(3),
};
list.Add(customer);
}
}
}
}
return list;
}
}
