test - mock dbcontext c#
NSubstitute DbSet/IQueryable<T> (5)
Escribí un contenedor hace aproximadamente un año en torno al mismo código al que hace referencia desde Testing with Your Own Test Doubles (EF6 en adelante) . Este contenedor se puede encontrar en GitHub DbContextMockForUnitTests . El propósito de este envoltorio es reducir la cantidad de código repetitivo / duplicado necesario para configurar las pruebas unitarias que utilizan EF en el que desea DbContext
y DbSets
. La mayor parte del código EF simulado que tiene en el PO puede reducirse a 2 líneas de código ( y solo 1 si está utilizando DbContext.Set<T>
lugar de las propiedades DbSet ) y el código falso se llama en el contenedor.
Para usarlo copie e incluya los archivos en la carpeta MockHelpers
en su proyecto de prueba.
Aquí hay una prueba de ejemplo que usa lo que tenía arriba, observe que ahora solo se necesitan 2 líneas de código para configurar el falso DbSet<T>
en el DbContext
.
public void GetAllBlogs_orders_by_name()
{
// Arrange
var data = new List<Blog>
{
new Blog { Name = "BBB" },
new Blog { Name = "ZZZ" },
new Blog { Name = "AAA" },
};
var mockContext = Substitute.For<BloggingContext>();
// Create and assign the substituted DbSet
var mockSet = data.GenerateMockDbSet();
mockContext.Blogs.Returns(mockSet);
// act
}
Es tan fácil hacer de esto una prueba que invoca algo que utiliza el patrón async / .ToListAsync()
como .ToListAsync()
en el DbSet<T>
.
public async Task GetAllBlogs_orders_by_name()
{
// Arrange
var data = new List<Blog>
{
new Blog { Name = "BBB" },
new Blog { Name = "ZZZ" },
new Blog { Name = "AAA" },
};
var mockContext = Substitute.For<BloggingContext>();
// Create and assign the substituted DbSet
var mockSet = data.GenerateMockDbSetForAsync(); // only change is the ForAsync version of the method
mockContext.Blogs.Returns(mockSet);
// act
}
Entonces, EntityFramework 6 es mucho mejor comprobable que las versiones anteriores. Y hay algunos buenos ejemplos en internet para frameworks como Moq, pero el caso es que prefiero usar NSubstitute. Obtuve los ejemplos de "no consulta" traducidos para que funcionen con el uso de NSubstitute, pero no puedo entender la ''query-test''.
Cómo funciona los elementos de Moq. items.As<IQueryable<T>>().Setup(m => m.Provider).Returns(data.Provider);
traducir a NSubstitute? Pensé algo así como ((IQueryable<T>) items).Provider.Returns(data.Provider);
pero eso no funcionó. También probé items.AsQueryable().Provider.Returns(data.Provider);
pero eso tampoco funcionó.
La excepción que recibo es:
"System.NotImplementedException: El miembro ''IQueryable.Provider'' no se ha implementado en el tipo ''DbSet
1Proxy'' which inherits from ''DbSet
1''. Los dobles de prueba para ''DbSet`1'' deben proporcionar implementaciones de métodos y propiedades que se usan."
Así que permítanme citar el ejemplo de código del enlace de arriba. Este ejemplo de código usa Moq para simular el DbContext y el DbSet.
public void GetAllBlogs_orders_by_name()
{
// Arrange
var data = new List<Blog>
{
new Blog { Name = "BBB" },
new Blog { Name = "ZZZ" },
new Blog { Name = "AAA" },
}.AsQueryable();
var mockSet = new Mock<DbSet<Blog>>();
mockSet.As<IQueryable<Blog>>().Setup(m => m.Provider).Returns(data.Provider);
mockSet.As<IQueryable<Blog>>().Setup(m => m.Expression).Returns(data.Expression);
mockSet.As<IQueryable<Blog>>().Setup(m => m.ElementType).Returns(data.ElementType);
mockSet.As<IQueryable<Blog>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());
var mockContext = new Mock<BloggingContext>();
mockContext.Setup(c => c.Blogs).Returns(mockSet.Object);
// ...
}
Y esto es lo lejos que vengo con NSubstitute
public void GetAllBlogs_orders_by_name()
{
// Arrange
var data = new List<Blog>
{
new Blog { Name = "BBB" },
new Blog { Name = "ZZZ" },
new Blog { Name = "AAA" },
}.AsQueryable();
var mockSet = Substitute.For<DbSet<Blog>>();
// it''s the next four lines I don''t get to work
((IQueryable<Blog>) mockSet).Provider.Returns(data.Provider);
((IQueryable<Blog>) mockSet).Expression.Returns(data.Expression);
((IQueryable<Blog>) mockSet).ElementType.Returns(data.ElementType);
((IQueryable<Blog>) mockSet).GetEnumerator().Returns(data.GetEnumerator());
var mockContext = Substitute.For<BloggingContext>();
mockContext.Blogs.Returns(mockSet);
// ...
}
Entonces la pregunta es; ¿Cómo puede uno Sustituir una propiedad de IQueryable (como Proveedor)?
Este es mi método estático genérico estático para generar DbSet falso. Puede ser útil.
public static class CustomTestUtils
{
public static DbSet<T> FakeDbSet<T>(List<T> data) where T : class
{
var _data = data.AsQueryable();
var fakeDbSet = Substitute.For<DbSet<T>, IQueryable<T>>();
((IQueryable<T>)fakeDbSet).Provider.Returns(_data.Provider);
((IQueryable<T>)fakeDbSet).Expression.Returns(_data.Expression);
((IQueryable<T>)fakeDbSet).ElementType.Returns(_data.ElementType);
((IQueryable<T>)fakeDbSet).GetEnumerator().Returns(_data.GetEnumerator());
fakeDbSet.AsNoTracking().Returns(fakeDbSet);
return fakeDbSet;
}
}
Esto sucede debido a la sintaxis NSubstitute específica. Por ejemplo en:
((IQueryable<Blog>) mockSet).Provider.Returns(data.Provider);
NSubstitute llama al getter del proveedor, luego especifica el valor de retorno. Esta llamada captada no es interceptada por el sustituto y usted recibe una excepción. Sucede debido a la implementación explícita de la propiedad IQueryable.Provider en la clase DbQuery.
Puede crear explícitamente sustitutos para múltiples interfaces con NSub, y crea un proxy que cubre todas las interfaces especificadas. Entonces las llamadas a las interfaces serán interceptadas por el sustituto. Por favor use la siguiente sintaxis:
// Create a substitute for DbSet and IQueryable types:
var mockSet = Substitute.For<DbSet<Blog>, IQueryable<Blog>>();
// And then as you do:
((IQueryable<Blog>)mockSet).Provider.Returns(data.Provider);
...
Gracias a Kevin, encontré el problema en la traducción de mi código.
Las muestras del código unittest se burlan de DbSet
, pero NSubstitute requiere la implementación de la interfaz. Así que el equivalente de Moqs new Mock<DbSet<Blog>>()
para NSubstitute es Substitute.For<IDbSet<Blog>>()
. No siempre se requiere que proporcione la interfaz, por eso me confundí. Pero en este caso específico, resultó ser crucial.
También resultó que no tenemos que convertir a Queryable cuando usamos la interfaz IDbSet.
Entonces el código de prueba de trabajo:
public void GetAllBlogs_orders_by_name()
{
// Arrange
var data = new List<Blog>
{
new Blog { Name = "BBB" },
new Blog { Name = "ZZZ" },
new Blog { Name = "AAA" },
}.AsQueryable();
var mockSet = Substitute.For<IDbSet<Blog>>();
mockSet.Provider.Returns(data.Provider);
mockSet.Expression.Returns(data.Expression);
mockSet.ElementType.Returns(data.ElementType);
mockSet.GetEnumerator().Returns(data.GetEnumerator());
var mockContext = Substitute.For<BloggingContext>();
mockContext.Blogs.Returns(mockSet);
// Act and Assert ...
}
Escribí un pequeño método de extensión para limpiar la sección Organizar de las pruebas unitarias.
public static class ExtentionMethods
{
public static IDbSet<T> Initialize<T>(this IDbSet<T> dbSet, IQueryable<T> data) where T : class
{
dbSet.Provider.Returns(data.Provider);
dbSet.Expression.Returns(data.Expression);
dbSet.ElementType.Returns(data.ElementType);
dbSet.GetEnumerator().Returns(data.GetEnumerator());
return dbSet;
}
}
// usage like:
var mockSet = Substitute.For<IDbSet<Blog>>().Initialize(data);
No es la pregunta, pero en caso de que también necesite soportar operaciones asincrónicas:
public static IDbSet<T> Initialize<T>(this IDbSet<T> dbSet, IQueryable<T> data) where T : class
{
dbSet.Provider.Returns(data.Provider);
dbSet.Expression.Returns(data.Expression);
dbSet.ElementType.Returns(data.ElementType);
dbSet.GetEnumerator().Returns(data.GetEnumerator());
if (dbSet is IDbAsyncEnumerable)
{
((IDbAsyncEnumerable<T>) dbSet).GetAsyncEnumerator()
.Returns(new TestDbAsyncEnumerator<T>(data.GetEnumerator()));
dbSet.Provider.Returns(new TestDbAsyncQueryProvider<T>(data.Provider));
}
return dbSet;
}
// create substitution with async
var mockSet = Substitute.For<IDbSet<Blog>, IDbAsyncEnumerable<Blog>>().Initialize(data);
// create substitution without async
var mockSet = Substitute.For<IDbSet<Blog>>().Initialize(data);
No debería necesitar burlarse de todas las piezas de IQueryable. Cuando uso NSubstitute para burlar un EF DbContext, hago algo como esto:
interface IContext
{
IDbSet<Foo> Foos { get; set; }
}
var context = Substitute.For<IContext>();
context.Foos.Returns(new MockDbSet<Foo>());
Con una implementación simple de IDbSet alrededor de una lista o algo para mi MockDbSet ().
En general, debería burlarse de las interfaces, no de los tipos, ya que NSubstitute solo anulará los métodos virtuales.