Pruebas con sus propios duplicados de prueba
Nota:
Solo EF6 y versiones posteriores: las características, las API, etc. que se tratan en esta página se han incluido a partir de Entity Framework 6. Si usa una versión anterior, no se aplica parte o la totalidad de la información.
Cuando creamos pruebas para aplicaciones, suele ser buena idea evitar consultar la base de datos. Entity Framework le permite hacerlo creando un contexto que usa datos en memoria y cuyo comportamiento lo definen las pruebas.
Opciones para crear duplicados de prueba
Hay dos enfoques diferentes que se pueden usar para crear una versión en memoria del contexto.
- Crear sus propios duplicados de prueba: este enfoque implica escribir su propia implementación en memoria del contexto y DbSets. Esto le proporciona un gran control sobre cómo se comportan las clases, pero podría implicar escribir y mantener muchas líneas de código.
- Uso de un marco de trabajo ficticio para duplicados de prueba: con un marco ficticio (como Moq) es posible tener las implementaciones en memoria del contexto y los conjuntos creados dinámicamente en tiempo de ejecución automáticamente.
En este artículo se tratará la creación de su propio duplicado de prueba. Para obtener información sobre el uso de un marco ficticio, consulte Pruebas con un marco ficticio.
Pruebas con versiones anteriores a EF6
El código que se muestra en este artículo es compatible con EF6. Para realizar pruebas con EF5 y versiones anteriores, consulte Pruebas con contextos falsos.
Limitaciones de los duplicados de prueba en memoria de EF
Los duplicados de prueba en memoria pueden proporcionar una buena cobertura de nivel de prueba unitaria de bits de aplicaciones que usen EF. Sin embargo, al hacer esto se usará LINQ to Objects para ejecutar consultas en datos en memoria. Esto podría dar lugar a un comportamiento diferente del uso del proveedor LINQ de EF (LINQ to Entities) para traducir consultas a SQL que se ejecuten en la base de datos.
Un ejemplo de esta diferencia es cargar datos relacionados. Si se crea una serie de blogs con entradas relacionadas, al usar datos en memoria las entradas relacionadas siempre se cargarán para cada blog. Sin embargo, cuando se ejecute en una base de datos, los datos solo se cargarán si se usa el método Include.
Por este motivo, se recomienda incluir siempre algún nivel de pruebas de un extremo a otro (además de las pruebas unitarias) para asegurarse de que la aplicación funcione correctamente en una base de datos.
Seguir con este artículo
En este artículo se proporcionan listas de código completas que se pueden copiar en Visual Studio para seguir el artículo si se desea. Es más fácil crear un proyecto de prueba unitaria y tendrá que tener como destino .NET Framework 4.5 para completar las secciones asincrónicas.
Creación de una interfaz de contexto
Examinemos las pruebas de un servicio que usa un modelo de EF. Para poder reemplazar nuestro contexto de EF por una versión en memoria para pruebas, definiremos una interfaz que implementará nuestro contexto de EF (y es doble en memoria).
El servicio que probaremos consultará y modificará los datos mediante las propiedades DbSet de nuestro contexto y también llamará a SaveChanges para insertar cambios en la base de datos. Por lo tanto, estamos incluyendo estos miembros en la interfaz.
using System.Data.Entity;
namespace TestingDemo
{
public interface IBloggingContext
{
DbSet<Blog> Blogs { get; }
DbSet<Post> Posts { get; }
int SaveChanges();
}
}
El modelo de EF
El servicio que vamos a probar usa un modelo de EF formado por las clases Blog y Post, y BloggingContext. El código podría haberlo generado el diseñador de EF o ser un modelo de Code First.
using System.Collections.Generic;
using System.Data.Entity;
namespace TestingDemo
{
public class BloggingContext : DbContext, IBloggingContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
}
public class Blog
{
public int BlogId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public virtual List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public virtual Blog Blog { get; set; }
}
}
Implementación de la interfaz de contexto con el diseñador de EF
Tenga en cuenta que nuestro contexto implementa la interfaz de IBloggingContext.
Si usa Code First, se puede editar el contexto directamente para implementar la interfaz. Si usa el diseñador de EF, deberá editar la plantilla T4 que genera el contexto. Abra el archivo <model_name>.Context.tt que está anidado en el archivo edmx, busque el siguiente fragmento de código y agregue la interfaz, tal y como se muestra.
<#=Accessibility.ForType(container)#> partial class <#=code.Escape(container)#> : DbContext, IBloggingContext
Servicio que se vaya a probar
Para demostrar las pruebas con duplicados de prueba en memoria, escribiremos un par de pruebas para un BlogService. El servicio es capaz de crear nuevos blogs (AddBlog) y devolver todos los blogs ordenados por nombre (GetAllBlogs). Además de GetAllBlogs, también hemos proporcionado un método que obtendrá de forma asincrónica todos los blogs ordenados por nombre (GetAllBlogsAsync).
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;
namespace TestingDemo
{
public class BlogService
{
private IBloggingContext _context;
public BlogService(IBloggingContext context)
{
_context = context;
}
public Blog AddBlog(string name, string url)
{
var blog = new Blog { Name = name, Url = url };
_context.Blogs.Add(blog);
_context.SaveChanges();
return blog;
}
public List<Blog> GetAllBlogs()
{
var query = from b in _context.Blogs
orderby b.Name
select b;
return query.ToList();
}
public async Task<List<Blog>> GetAllBlogsAsync()
{
var query = from b in _context.Blogs
orderby b.Name
select b;
return await query.ToListAsync();
}
}
}
Creación de los duplicados de prueba en memoria
Ahora que tenemos el modelo EF real y el servicio que puede usarlo, es el momento de crear el duplicado de prueba en memoria que se puede usar para las pruebas. Hemos creado un duplicado de prueba TestContext para nuestro contexto. En los duplicados de prueba, es posible elegir el comportamiento que queramos para admitir las pruebas que ejecutaremos. En este ejemplo, solo se captura el número de veces que se llama a SaveChanges, pero se puede incluir la lógica necesaria para comprobar el escenario que esté probando.
También hemos creado un TestDbSet que proporciona una implementación en memoria de DbSet. Hemos proporcionado una implementación completa para todos los métodos de DbSet (excepto para Find), pero solo es necesario implementar los miembros que usará su escenario de prueba.
TestDbSet usa otras clases de infraestructura que hemos incluido para asegurarnos de que se puedan procesar las consultas asincrónicas.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
namespace TestingDemo
{
public class TestContext : IBloggingContext
{
public TestContext()
{
this.Blogs = new TestDbSet<Blog>();
this.Posts = new TestDbSet<Post>();
}
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
public int SaveChangesCount { get; private set; }
public int SaveChanges()
{
this.SaveChangesCount++;
return 1;
}
}
public class TestDbSet<TEntity> : DbSet<TEntity>, IQueryable, IEnumerable<TEntity>, IDbAsyncEnumerable<TEntity>
where TEntity : class
{
ObservableCollection<TEntity> _data;
IQueryable _query;
public TestDbSet()
{
_data = new ObservableCollection<TEntity>();
_query = _data.AsQueryable();
}
public override TEntity Add(TEntity item)
{
_data.Add(item);
return item;
}
public override TEntity Remove(TEntity item)
{
_data.Remove(item);
return item;
}
public override TEntity Attach(TEntity item)
{
_data.Add(item);
return item;
}
public override TEntity Create()
{
return Activator.CreateInstance<TEntity>();
}
public override TDerivedEntity Create<TDerivedEntity>()
{
return Activator.CreateInstance<TDerivedEntity>();
}
public override ObservableCollection<TEntity> Local
{
get { return _data; }
}
Type IQueryable.ElementType
{
get { return _query.ElementType; }
}
Expression IQueryable.Expression
{
get { return _query.Expression; }
}
IQueryProvider IQueryable.Provider
{
get { return new TestDbAsyncQueryProvider<TEntity>(_query.Provider); }
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return _data.GetEnumerator();
}
IEnumerator<TEntity> IEnumerable<TEntity>.GetEnumerator()
{
return _data.GetEnumerator();
}
IDbAsyncEnumerator<TEntity> IDbAsyncEnumerable<TEntity>.GetAsyncEnumerator()
{
return new TestDbAsyncEnumerator<TEntity>(_data.GetEnumerator());
}
}
internal class TestDbAsyncQueryProvider<TEntity> : IDbAsyncQueryProvider
{
private readonly IQueryProvider _inner;
internal TestDbAsyncQueryProvider(IQueryProvider inner)
{
_inner = inner;
}
public IQueryable CreateQuery(Expression expression)
{
return new TestDbAsyncEnumerable<TEntity>(expression);
}
public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
{
return new TestDbAsyncEnumerable<TElement>(expression);
}
public object Execute(Expression expression)
{
return _inner.Execute(expression);
}
public TResult Execute<TResult>(Expression expression)
{
return _inner.Execute<TResult>(expression);
}
public Task<object> ExecuteAsync(Expression expression, CancellationToken cancellationToken)
{
return Task.FromResult(Execute(expression));
}
public Task<TResult> ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
{
return Task.FromResult(Execute<TResult>(expression));
}
}
internal class TestDbAsyncEnumerable<T> : EnumerableQuery<T>, IDbAsyncEnumerable<T>, IQueryable<T>
{
public TestDbAsyncEnumerable(IEnumerable<T> enumerable)
: base(enumerable)
{ }
public TestDbAsyncEnumerable(Expression expression)
: base(expression)
{ }
public IDbAsyncEnumerator<T> GetAsyncEnumerator()
{
return new TestDbAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
}
IDbAsyncEnumerator IDbAsyncEnumerable.GetAsyncEnumerator()
{
return GetAsyncEnumerator();
}
IQueryProvider IQueryable.Provider
{
get { return new TestDbAsyncQueryProvider<T>(this); }
}
}
internal class TestDbAsyncEnumerator<T> : IDbAsyncEnumerator<T>
{
private readonly IEnumerator<T> _inner;
public TestDbAsyncEnumerator(IEnumerator<T> inner)
{
_inner = inner;
}
public void Dispose()
{
_inner.Dispose();
}
public Task<bool> MoveNextAsync(CancellationToken cancellationToken)
{
return Task.FromResult(_inner.MoveNext());
}
public T Current
{
get { return _inner.Current; }
}
object IDbAsyncEnumerator.Current
{
get { return Current; }
}
}
}
Implementación de Find
El método Find es difícil de implementar de forma genérica. Si necesitase probar el código que use el método Find, resulta más fácil crear un DbSet de prueba para cada uno de los tipos de entidad que necesiten admitir la búsqueda. A continuación, puede escribir lógica para buscar ese tipo de entidad determinado, tal y como se muestra a continuación.
using System.Linq;
namespace TestingDemo
{
class TestBlogDbSet : TestDbSet<Blog>
{
public override Blog Find(params object[] keyValues)
{
var id = (int)keyValues.Single();
return this.SingleOrDefault(b => b.BlogId == id);
}
}
}
Escribiendo algunas pruebas
Eso es todo lo necesario para empezar a probar. La prueba siguiente crea un TestContext y, a continuación, un servicio basado en este contexto. A continuación, el servicio se usará para crear un nuevo blog mediante el método AddBlog. Por último, la prueba verificará que el servicio haya agregado un nuevo blog a la propiedad de Blogs de contexto y llamado a SaveChanges en el contexto.
Este es solo un ejemplo de los tipos de cosas que se pueden probar con un duplicado de prueba en memoria, y puede ajustar la lógica de los duplicados de prueba y la comprobación para cumplir con sus requisitos.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;
namespace TestingDemo
{
[TestClass]
public class NonQueryTests
{
[TestMethod]
public void CreateBlog_saves_a_blog_via_context()
{
var context = new TestContext();
var service = new BlogService(context);
service.AddBlog("ADO.NET Blog", "http://blogs.msdn.com/adonet");
Assert.AreEqual(1, context.Blogs.Count());
Assert.AreEqual("ADO.NET Blog", context.Blogs.Single().Name);
Assert.AreEqual("http://blogs.msdn.com/adonet", context.Blogs.Single().Url);
Assert.AreEqual(1, context.SaveChangesCount);
}
}
}
Aquí hay otra prueba de ejemplo: esta vez una que realiza una consulta. La prueba comienza con la creación de un contexto de prueba con algunos datos en su propiedad Blog: tenga en cuenta que los datos no están en orden alfabético. A continuación, podemos crear un BlogService basado en nuestros contextos de prueba y asegurarnos de que los datos que recuperemos de GetAllBlogs se ordenen por nombre.
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace TestingDemo
{
[TestClass]
public class QueryTests
{
[TestMethod]
public void GetAllBlogs_orders_by_name()
{
var context = new TestContext();
context.Blogs.Add(new Blog { Name = "BBB" });
context.Blogs.Add(new Blog { Name = "ZZZ" });
context.Blogs.Add(new Blog { Name = "AAA" });
var service = new BlogService(context);
var blogs = service.GetAllBlogs();
Assert.AreEqual(3, blogs.Count);
Assert.AreEqual("AAA", blogs[0].Name);
Assert.AreEqual("BBB", blogs[1].Name);
Assert.AreEqual("ZZZ", blogs[2].Name);
}
}
}
Por último, escribiremos una prueba más que use nuestro método asincrónico para asegurarnos de que la infraestructura asincrónica que hemos incluido en TestDbSet funcione.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace TestingDemo
{
[TestClass]
public class AsyncQueryTests
{
[TestMethod]
public async Task GetAllBlogsAsync_orders_by_name()
{
var context = new TestContext();
context.Blogs.Add(new Blog { Name = "BBB" });
context.Blogs.Add(new Blog { Name = "ZZZ" });
context.Blogs.Add(new Blog { Name = "AAA" });
var service = new BlogService(context);
var blogs = await service.GetAllBlogsAsync();
Assert.AreEqual(3, blogs.Count);
Assert.AreEqual("AAA", blogs[0].Name);
Assert.AreEqual("BBB", blogs[1].Name);
Assert.AreEqual("ZZZ", blogs[2].Name);
}
}
}