C#:cómo realizar una prueba unitaria del código que utiliza Dapper

C#:cómo realizar una prueba unitaria del código que utiliza Dapper

Dapper hace que su código sea difícil de probar. El problema es que Dapper usa métodos de extensión estáticos y los métodos estáticos son difíciles de simular.

Un enfoque es envolver los métodos estáticos de Dapper en una clase, extraer una interfaz para esa clase contenedora y luego inyectar la dependencia de la interfaz contenedora. En las pruebas unitarias, puede simular la interfaz del contenedor.

En este artículo, mostraré cómo hacer este enfoque.

Primero, el código del repositorio usando Dapper

Comencemos mirando el código del repositorio que usa Dapper para ejecutar una consulta:

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)

Para hacer que esta unidad de código sea comprobable, necesitamos simular el método de conexión estática.Query(). En este momento, en realidad se está conectando a la base de datos y ejecutando la consulta.

Podemos usar la técnica explicada en este artículo sobre la simulación de métodos estáticos:

  • Envuelva las llamadas a métodos estáticos en una clase y extraiga una interfaz para el contenedor.
  • La dependencia inyecta la interfaz en el repositorio.
  • En las pruebas unitarias, simule la interfaz contenedora y pásela al repositorio.

Envuelva el método Dapper estático

Cree una clase y ajuste el método Query() estático:

using Dapper;

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

Tenga en cuenta que esto no pasa todos los parámetros opcionales que utiliza el método Dapper. Esto simplifica un poco las cosas. Si realmente no está utilizando los otros parámetros, también podría dejarlos fuera de la clase contenedora.

Ahora extraiga una interfaz de la clase contenedora:

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

La dependencia inyecta la interfaz contenedora en el repositorio

Agregue IDapperWrapper como parámetro de construcción en MovieRepository:

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

Escriba una prueba unitaria y simule el envoltorio

La siguiente prueba verifica que el repositorio esté usando DapperWrapper para ejecutar la consulta SQL esperada con un objeto IDbConnection creado correctamente:

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

Al principio, esta prueba fallará porque el código no se actualizó para usar DapperWrapper, por lo que todavía está intentando conectarse a la base de datos (que se agota después de 15 segundos y genera una excepción).

Bien, actualicemos el código para usar 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)

Ahora pasa la prueba.

Como se está burlando de Dapper, en realidad no se está conectando a la base de datos. Esto hace que la prueba sea determinista y rápida, dos cualidades de una buena prueba unitaria.

Prueba unitaria de una consulta parametrizada

Actualización:se agregó esta nueva sección el 19 de octubre de 2021.

En esta sección, mostraré cómo hacer el mismo enfoque que se muestra arriba para probar unitariamente una consulta parametrizada.

Supongamos que desea realizar una prueba unitaria de la siguiente consulta parametrizada:

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 – Envolver el método Query()

Cuando está ejecutando una consulta parametrizada con Dapper, debe pasar el parámetro de objeto parámetro. Entonces, en DapperWrapper, envuelve esta variación del método 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)

Nota:'object param' es un parámetro opcional de Query() en Dapper. Para mantener el contenedor lo más simple posible, es mejor no tener parámetros opcionales. Agregue sobrecargas con el parámetro en su lugar.

2 – Actualizar el método para usar el contenedor

Reemplace la llamada a connection.Query() con 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 – Simular el método de envoltorio

Normalmente, cuando ejecuta consultas parametrizadas con Dapper, pasa un tipo anónimo con los parámetros de consulta. Esto mantiene las cosas agradables y limpias. Sin embargo, esto hace que sea un poco complicado configurar el simulacro.

Hay tres opciones que puede hacer para especificar el parámetro de objeto parámetro en la configuración simulada.

Opción 1:usar It.IsAny()

Si no le preocupa hacer coincidir con precisión el parámetro de objeto parámetro, puede usar It.IsAny() en la configuración simulada:

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

Opción 2:Usar It.Is + reflejo

Si desea verificar los valores en el tipo anónimo, puede usar It.Is con reflexión:

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)

Opción 3:Pase en un tipo no anónimo

La dificultad para configurar el simulacro se debe a que se trata del tipo anónimo. En su lugar, puede pasar un tipo no anónimo, lo que simplifica la configuración simulada.

Primero, cambie el código en el repositorio pasando un tipo no anónimo. En este ejemplo, la Película existente la clase se puede usar para esto.

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)

La configuración simulada puede verificar este parámetro directamente:

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)