C# – Wie man Code testet, der Dapper verwendet

C# – Wie man Code testet, der Dapper verwendet

Dapper erschwert den Unit-Test Ihres Codes. Das Problem ist, dass Dapper statische Erweiterungsmethoden verwendet und statische Methoden schwer zu verspotten sind.

Ein Ansatz besteht darin, die statischen Dapper-Methoden in eine Klasse einzuschließen, eine Schnittstelle für diese Wrapper-Klasse zu extrahieren und dann die Wrapper-Schnittstelle durch Abhängigkeit einzufügen. In den Unit-Tests können Sie dann die Wrapper-Schnittstelle verspotten.

In diesem Artikel zeige ich, wie man diesen Ansatz durchführt.

Zunächst der Repository-Code mit Dapper

Sehen wir uns zunächst den Repository-Code an, der Dapper verwendet, um eine Abfrage auszuführen:

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)

Um diese Code-Unit testbar zu machen, müssen wir die statische Methode connection.Query() verspotten. Im Moment verbindet sich dies tatsächlich mit der Datenbank und führt die Abfrage aus.

Wir können die in diesem Artikel erläuterte Technik zum Verspotten statischer Methoden verwenden:

  • Wrapper die statischen Methodenaufrufe in eine Klasse und extrahiere eine Schnittstelle für den Wrapper.
  • Abhängigkeit fügt die Schnittstelle in das Repository ein.
  • Verspotten Sie in den Komponententests die Wrapper-Schnittstelle und übergeben Sie sie an das Repository.

Wrap die statische Dapper-Methode

Erstellen Sie eine Klasse und umschließen Sie die statische Query()-Methode:

using Dapper;

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

Beachten Sie, dass dadurch nicht alle optionalen Parameter übergeben werden, die die Dapper-Methode verwendet. Das vereinfacht die Sache ein wenig. Wenn Sie die anderen Parameter wirklich nicht verwenden, können Sie sie genauso gut aus der Wrapper-Klasse herauslassen.

Extrahieren Sie nun eine Schnittstelle aus der Wrapper-Klasse:

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

Abhängigkeit fügt die Wrapper-Schnittstelle in das Repository ein

Fügen Sie IDapperWrapper als Konstruktorparameter in MovieRepository hinzu:

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

Schreiben Sie einen Komponententest und verspotten Sie den Wrapper

Der folgende Test überprüft, ob das Repository DapperWrapper verwendet, um die erwartete SQL-Abfrage mit einem ordnungsgemäß erstellten IDbConnection-Objekt auszuführen:

[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)

Zuerst schlägt dieser Test fehl, weil der Code nicht aktualisiert wurde, um DapperWrapper tatsächlich zu verwenden, also versucht er immer noch, sich tatsächlich mit der Datenbank zu verbinden (was nach 15 Sekunden abläuft und eine Ausnahme auslöst).

Ok, aktualisieren wir den Code, um DapperWrapper zu verwenden:

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)

Jetzt ist der Test bestanden.

Da es Dapper verspottet, verbindet es sich nicht wirklich mit der Datenbank. Dadurch wird der Test deterministisch und schnell – zwei Qualitäten eines guten Unit-Tests.

Komponententest einer parametrisierten Abfrage

Aktualisierung:Dieser neue Abschnitt wurde am 19.10.2021 hinzugefügt.

In diesem Abschnitt zeige ich, wie Sie den oben gezeigten Ansatz zum Komponententest einer parametrisierten Abfrage verwenden.

Angenommen, Sie möchten die folgende parametrisierte Abfrage komponententesten:

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 – Umschließen Sie die Query()-Methode

Wenn Sie eine parametrisierte Abfrage mit Dapper ausführen, müssen Sie den Objektparameter übergeben Parameter. Umschließen Sie also in DapperWrapper diese Variante der Query()-Methode:

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)

Hinweis:„object param“ ist ein optionaler Parameter von Query() in Dapper. Um den Wrapper so einfach wie möglich zu halten, ist es besser, keine optionalen Parameter zu haben. Fügen Sie stattdessen Überladungen mit dem Parameter hinzu.

2 – Aktualisieren Sie die Methode, um den Wrapper zu verwenden

Ersetzen Sie den Aufruf von connection.Query() durch 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 – Mock die Wrapper-Methode

Wenn Sie parametrisierte Abfragen mit Dapper ausführen, übergeben Sie normalerweise einen anonymen Typ mit den Abfrageparametern. So bleibt alles schön sauber. Dies macht es jedoch etwas schwierig, den Mock einzurichten.

Es gibt drei Möglichkeiten, den Objektparameter anzugeben Parameter im Mock-Setup.

Option 1 – Verwenden Sie It.IsAny()

Wenn Sie sich keine Gedanken über die genaue Übereinstimmung mit dem Objektparameter machen -Parameter können Sie It.IsAny() im Mock-Setup verwenden:

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

Option 2 – Verwenden Sie It.Is + Reflektion

Wenn Sie die Werte des anonymen Typs überprüfen möchten, können Sie It.Is mit Reflection:

verwenden
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)

Option 3 – Übergeben Sie einen nicht anonymen Typ

Die Schwierigkeit beim Einrichten des Scheins wird durch den Umgang mit dem anonymen Typ verursacht. Sie können stattdessen einen nicht anonymen Typ übergeben, was die simulierte Einrichtung vereinfacht.

Ändern Sie zunächst den Code im Repository, indem Sie einen nicht anonymen Typ übergeben. In diesem Beispiel der vorhandene Film Klasse kann dafür verwendet werden.

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)

Das Mock-Setup kann diesen Parameter dann direkt prüfen:

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)