はじめに
データベースを読み書きする部分のユニットテストがやりにくいのには、いくつか理由があります。
- 複数人でテストを同時に実行すると、競合する
- データベースを使ったテストは、時間が掛かる
- データベース内のデータが変わると、テストが失敗する
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; } }