C# – Dapper を使用するコードを単体テストする方法

Dapper を使用すると、コードの単体テストが難しくなります。問題は、Dapper が静的拡張メソッドを使用しており、静的メソッドをモックアウトするのが難しいことです。

1 つの方法は、Dapper の静的メソッドをクラスでラップし、そのラッパー クラスのインターフェイスを抽出してから、ラッパー インターフェイスに依存関係を注入することです。単体テストでは、ラッパー インターフェースのモックを作成できます。

この記事では、このアプローチを行う方法を示します。

まず、Dapper を使用したリポジトリ コード

まず、Dapper を使用してクエリを実行しているリポジトリ コードを見てみましょう。

public class MovieRepository
{
	private readonly string ConnectionString;
	public MovieRepository(string connectionString)
	{
		ConnectionString = connectionString;
	}

	public IEnumerable<Movie> GetMovies()
	{
		using(var connection = new SqlConnection(ConnectionString))
		{
			return connection.Query<Movie>("SELECT Name, Description, RuntimeMinutes, Year FROM Movies");
		}
	}
}
Code language: C# (cs)

このコード ユニットをテスト可能にするには、静的な connection.Query() メソッドをモック アウトする必要があります。現在、これは実際にデータベースに接続してクエリを実行しています。

この記事で説明されている静的メソッドのモックアウトに関する手法を使用できます。

  • 静的メソッド呼び出しをクラスでラップし、ラッパーのインターフェースを抽出します。
  • インターフェースをレポジトリに注入する依存性
  • 単体テストでは、ラッパー インターフェースをモックアウトしてリポジトリに渡します。

静的 Dapper メソッドをラップする

クラスを作成し、静的な Query() メソッドをラップします:

using Dapper;

public class DapperWrapper : IDapperWrapper
{
	public IEnumerable<T> Query<T>(IDbConnection connection, string sql)
	{
		return connection.Query<T>(sql);
	}
}
Code language: C# (cs)

これは、Dapper メソッドが使用するすべてのオプション パラメータを渡しているわけではないことに注意してください。これにより、物事が少し単純化されます。他のパラメーターを実際に使用していない場合は、ラッパー クラスから除外することもできます。

ラッパー クラスからインターフェイスを抽出します。

public interface IDapperWrapper
{
	IEnumerable<T> Query<T>(IDbConnection connection, string sql);
}
Code language: C# (cs)

ラッパー インターフェースをリポジトリに依存性注入

IDapperWrapper を MovieRepository のコンストラクター パラメーターとして追加します。

private readonly IDapperWrapper DapperWrapper;
public MovieRepository(string connectionString, IDapperWrapper dapperWrapper)
{
	ConnectionString = connectionString;
	DapperWrapper = dapperWrapper;
}
Code language: C# (cs)

単体テストを作成し、ラッパーのモックを作成します

次のテストでは、リポジトリが DapperWrapper を使用して、適切に構築された IDbConnection オブジェクトで期待される SQL クエリを実行していることを確認します:

[TestMethod()]
public void GetMoviesTest_ReturnsMoviesFromQueryUsingExpectedSQLQueryAndConnectionString()
{
	//arrange
	var mockDapper = new Mock<IDapperWrapper>();
	var expectedConnectionString = @"Server=SERVERNAME;Database=TESTDB;Integrated Security=true;";
	var expectedQuery = "SELECT Name, Description, RuntimeMinutes, Year FROM Movies";
	var repo = new MovieRepository(expectedConnectionString, mockDapper.Object);
	var expectedMovies = new List<Movie>() { new Movie() { Name = "Test" } };

	mockDapper.Setup(t => t.Query<Movie>(It.Is<IDbConnection>(db => db.ConnectionString == expectedConnectionString), expectedQuery))
		.Returns(expectedMovies);

	//act
	var movies = repo.GetMovies();

	//assert
	Assert.AreSame(expectedMovies, movies);
}
Code language: C# (cs)

最初は、コードが実際に DapperWrapper を使用するように更新されていないため、このテストは失敗します。そのため、まだ実際にデータベースに接続しようとしています (15 秒後にタイムアウトになり、例外がスローされます)。

では、DapperWrapper を使用するようにコードを更新しましょう:

public IEnumerable<Movie> GetMovies()
{
	using(var connection = new SqlConnection(ConnectionString))
	{
		return DapperWrapper.Query<Movie>(connection, "SELECT Name, Description, RuntimeMinutes, Year FROM Movies");
	}
}
Code language: C# (cs)

これでテストはパスします。

Dapper をモックアウトしているため、実際にはデータベースに接続していません。これにより、テストが決定論的かつ高速になります。これは、優れた単体テストの 2 つの性質です。

パラメータ化されたクエリの単体テスト

更新:この新しいセクションを 2021 年 10 月 19 日に追加しました。

このセクションでは、上記と同じアプローチを使用して、パラメーター化されたクエリを単体テストする方法を示します。

次のパラメータ化されたクエリを単体テストするとします。

public IEnumerable<Movie> GetMoviesWithYear(int year)
{
	using (var connection = new SqlConnection(ConnectionString))
	{
		return connection.Query<Movie>("SELECT * FROM Movies WHERE Year=@year", new { year });
	}
}
Code language: C# (cs)

1 – Query() メソッドをラップする

Dapper でパラメーター化されたクエリを実行する場合、object param を渡す必要があります パラメータ。したがって、DapperWrapper では、この Query() メソッドのバリエーションをラップします。

public class DapperWrapper : IDapperWrapper
{
	public IEnumerable<T> Query<T>(IDbConnection connection, string sql)
	{
		return connection.Query<T>(sql);
	}
	public IEnumerable<T> Query<T>(IDbConnection connection, string sql, object param)
	{
		return connection.Query<T>(sql, param);
	}
}
Code language: C# (cs)

注:「オブジェクト パラメータ」は、Dapper の Query() のオプション パラメータです。ラッパーをできるだけシンプルに保つには、オプションのパラメーターを持たない方がよいでしょう。代わりに、パラメーターを使用してオーバーロードを追加してください。

2 – ラッパーを使用するようにメソッドを更新します

connection.Query() への呼び出しを DapperWrapper.Query() に置き換えます:

public IEnumerable<Movie> GetMoviesWithYear(int year)
{
	using (var connection = new SqlConnection(ConnectionString))
	{
		return DapperWrapper.Query<Movie>(connection, "SELECT * FROM Movies WHERE Year=@year", 
			new { year });
	}
}
Code language: C# (cs)

3 – ラッパー メソッドのモック

通常、Dapper でパラメーター化されたクエリを実行するときは、クエリ パラメーターと共に匿名型を渡します。これにより、物事がきれいに保たれます。ただし、これにより、モックのセットアップが少し難しくなります。

オブジェクト パラメータを指定するために実行できるオプションは 3 つあります。 モック セットアップのパラメータ。

オプション 1 – It.IsAny() を使用する

オブジェクト パラメータを正確に一致させることに関心がない場合 パラメータ、モック セットアップで It.IsAny() を使用できます:

mockDapper.Setup(t => t.Query<Movie>(It.Is<IDbConnection>(db => db.ConnectionString == expectedConnectionString), 
	expectedQuery,
	It.IsAny<object>()))
	.Returns(expectedMovies);
Code language: C# (cs)

オプション 2 – It.Is + リフレクション を使用

匿名型の値を確認したい場合は、 It.Is をリフレクションで使用できます:

mockDapper.Setup(t => t.Query<Movie>(It.Is<IDbConnection>(db => db.ConnectionString == expectedConnectionString), 
	expectedQuery,
	It.Is<object>(m => (int)m.GetType().GetProperty("year").GetValue(m) == 2010)))
	.Returns(expectedMovies);
Code language: C# (cs)

オプション 3 – 非匿名型を渡す

モックの設定が難しいのは、匿名型を扱うことが原因です。代わりに非匿名型を渡すことができます。これにより、モックのセットアップが簡素化されます。

まず、非匿名型を渡して、リポジトリ内のコードを変更します。この例では、既存の Movie クラスをこれに使用できます。

public IEnumerable<Movie> GetMoviesWithYear(int year)
{
	using (var connection = new SqlConnection(ConnectionString))
	{
		return DapperWrapper.Query<Movie>(connection, "SELECT * FROM Movies WHERE Year=@year", 
			new Movie() { Year = year });
	}
}
Code language: C# (cs)

モック セットアップは、このパラメーターを直接チェックできます。

mockDapper.Setup(t => t.Query<Movie>(It.Is<IDbConnection>(db => db.ConnectionString == expectedConnectionString), 
	expectedQuery,
	It.Is<Movie>(m => m.Year == 2010)))
	.Returns(expectedMovies);
Code language: C# (cs)