とりあえず作ってみたコードの問題点
この製品コードは、分岐構造が比較的単純で一気に書くことができたとはいうものの、ゴチャゴチャしていて読み辛いですね。また、次のような問題を抱えています。
- 接続文字列が埋め込まれているため、データベースを切り替えるためには何か所も修正が必要になる。
-
ロジックが、
SqlServerCe名前空間に依存している(SqlCeConnectionとSqlCeCommand)。ロジックは、できるだけ環境に依存しないコードにしたい。 -
前項と関連するが、モックやダミーに置き換えようと思うと、
SqlServerCe名前空間のクラスをいくつも置き換えなければならない。 - データベースの内容が変わると、テストが失敗してしまう。
このあとのストーリー
まずリファクタリングしていきます。接続文字列の部分を分離するところから始めて、モックやダミーへの置き換えを困難にしているSqlServerCe名前空間のクラスを使っている部分を、ビジネスロジックから順番に切り離していきます。
それから、テスト実行時にデータベースの内容を初期化する例と、トランザクションを利用してテスト中の変更をキャンセルする方法を紹介します。
接続オブジェクトの生成を分離する
まず、接続オブジェクトを生成するコードを、ロジックから分離します。この部分は、サンプルコードでは1か所だけですが、実際にはデータベースにアクセスするコードすべてに存在するでしょう。データベースにアクセスするメソッドを2つ書いたら、分離して重複を排除しなければならないことに気づくはずです。
[TestCase]
public void GetConnectionTest() {
string 接続文字列 = @"Data Source=C:\Program Files (x86)\Microsoft SQL Server Compact Edition\v3.5\Samples\Northwind.sdf";
SqlCeConnection conn = DbConnectionProvider.Connection;
Assert.AreEqual(接続文字列, conn.ConnectionString);
}
internal class DbConnectionProvider {
// 注) AssemblyInfo.cs に [assembly: InternalsVisibleTo("CsTdd07Test")] が必要
public static SqlCeConnection Connection {
get {
return new SqlCeConnection(@"Data Source=C:\Program Files (x86)\Microsoft SQL Server Compact Edition\v3.5\Samples\Northwind.sdf");
}
}
}
この実装は、実際にはテストと本番で切り替えできるように作ります。その方法は、次のように何通りも考えられます。
- config ファイルから読み込む
- テスト実装を組み込む (連載第6回を参照)
-
DIを使う
…etc.
このようにして作ったDbConnectionProviderを使うと、接続文字列を使わないようにロジックをリファクタリングできます。
//using (SqlCeConnection conn = new SqlCeConnection(@"Data Source= …
// ↓
using (SqlCeConnection conn = DbConnectionProvider.Connection) {
SqlServerCeへの依存を分離する
ロジックのコードでは、SqlServerCe名前空間のオブジェクトとしてSqlCeConnectionとSqlCeCommandを使っています。こういう特定の環境に依存したコードは、ロジックから分離しておきたいものです。
SqlCeCommandの生成を分離する
SqlCeCommandは、ロジックの中では汎用のDbCommandとして扱っています。まずSqlCeCommandの生成部分を分離するところから始めてみましょう。新たにDbCommandHelperクラスを作ることにして、そのインスタンスにSqlCeCommandを作ってもらうことにします。
SqlCeCommandを作ってもらうテスト
string 接続文字列 =
@"Data Source=C:\Program Files (x86)\Microsoft SQL Server Compact Edition\v3.5\Samples\Northwind.sdf";
string cmdText =
@"SELECT Country, [Company Name], [Customer ID], [Contact Name]
FROM Customers
WHERE ([Contact Name] LIKE @p1) OR ([Contact Name] LIKE @p2)";
[TestCase] //製品コード: CreateCommand()メソッドを実装
public void CreateCommandTest() {
DbCommand cmd = (new DbCommandHelper()).CreateCommand(cmdText);
Assert.IsInstanceOf<SqlCeCommand>(cmd);
Assert.AreEqual(cmdText, cmd.CommandText);
// Connectionも、自動的に付けてくれるとありがたい
Assert.AreEqual(接続文字列, cmd.Connection.ConnectionString);
Assert.AreEqual(ConnectionState.Open, cmd.Connection.State);
}
ここで、Connectionも自動的に付けてくれるようにしてしまいました。作ってもらったSqlCeCommandは、呼び出し側でDispose()すればよいのですが、自動的に付けてくれたConnectionのDispose()(あるいはClose())は、どこで実行すれば良いでしょう?いろんな方法が考えられますが、DbCommandHelperをDispose()するときに実行してやると便利そうです。
実際には、ここまでを実装したDbCommandHelperクラスを使って製品コードをリファクタリングしてから先に進みますが、ここでは、テストコードだけをまとめてお見せします。
DbCommandHelperにIDisposableを実装するテスト
[TestCase] //製品コード: IDisposable インターフェースと Connection プロパティを実装
public void DisposeTest() {
DbCommandHelper helper = new DbCommandHelper();
Assert.IsInstanceOf<IDisposable>(helper);
var conn = helper.Connection;
Assert.AreEqual(ConnectionState.Open, conn.State);
helper.Dispose();
Assert.AreEqual(ConnectionState.Closed, conn.State);
Assert.IsNull(helper.Connection);
}
[TestCase] //製品コード: if (_connection != null) を追加
public void DisposeTest_Connection生成前にDispose() {
DbCommandHelper helper = new DbCommandHelper();
helper.Dispose(); //最初はnull参照でREDになる。
Assert.IsNull(helper.Connection);
}
[TestCase] //製品コード: CreateCommand()内の cmd.Connection = … を修正
public void DisposeTest_CreateCommandメソッドでも同じ接続オブジェクトを使う() {
using (DbCommandHelper helper = new DbCommandHelper())
using(DbCommand cmd = helper.CreateCommand(cmdText)){
Assert.AreSame(helper.Connection, cmd.Connection);
}
}
