Compartilhar via


Testando com uma estrutura de simulação

Observação

Somente EF6– os recursos, as APIs etc. discutidos nesta página foram introduzidos no o Entity Framework 6. Se você estiver usando uma versão anterior, algumas ou todas as informações não se aplicarão.

Ao escrever testes para seu aplicativo, geralmente é desejável evitar atingir o banco de dados. O Entity Framework permite que você consiga isso criando um contexto – com o comportamento definido por seus testes – que usa dados na memória.

Opções para criar duplas de teste

Há duas abordagens diferentes que podem ser usadas para criar uma versão na memória do seu contexto.

  • Crie seus próprios duplos de teste – essa abordagem envolve implementar em memória seu próprio contexto e DbSets. Isso oferece muito controle sobre como as classes se comportam, mas podem envolver escrever e possuir uma quantidade razoável de código.
  • Use uma estrutura de simulação para criar duplas de teste – usando uma estrutura de simulação (como o Moq), você pode ter as implementações na memória de seu contexto e conjuntos criados dinamicamente em runtime para você.

Este artigo lidará com o uso de uma estrutura de simulação. Para criar suas próprias duplas de teste, consulte Testando com Suas Próprias Duplas de Teste.

Para demonstrar o uso do EF com uma estrutura de simulação, usaremos o Moq. A maneira mais fácil de obter o Moq é instalar o pacote Moq do NuGet.

Testes com versões pré-EF6

O cenário mostrado neste artigo depende de algumas alterações feitas no DbSet no EF6. Para testar com o EF5 e a versão anterior, consulte Testando com um Contexto Falso.

Limitações dos dublês de teste em memória do EF

Os dublês de teste em memória podem ser uma boa maneira de fornecer cobertura de teste de unidade para partes do seu aplicativo que utilizam o Entity Framework (EF). No entanto, ao fazer isso, você está usando LINQ to Objects para executar consultas em dados na memória. Isso pode resultar em um comportamento diferente do uso do provedor LINQ (LINQ to Entities) do EF para traduzir consultas para SQL que são executadas em seu banco de dados.

Um exemplo dessa diferença é o carregamento de dados relacionados. Se você criar uma série de Blogs com Postagens relacionadas, ao usar dados na memória, as Postagens relacionadas sempre serão carregadas para cada Blog. No entanto, ao executar em um banco de dados, os dados serão carregados somente se você usar o método Include.

Por esse motivo, é recomendável sempre incluir algum nível de teste de ponta a ponta (além dos testes de unidade) para garantir que seu aplicativo funcione corretamente em um banco de dados.

Seguindo as instruções deste artigo

Este artigo fornece listagens de código completas que você pode copiar para o Visual Studio para acompanhar, se desejar. É mais fácil criar um Projeto de Teste de Unidade e você precisará destinar o .NET Framework 4.5 para concluir as seções que usam operações assíncronas.

O modelo EF

O serviço que vamos testar usa um modelo EF composto pelas classes BloggingContext e Blog e Post. Esse código pode ter sido gerado pelo Designer EF ou ser um modelo code first.

using System.Collections.Generic;
using System.Data.Entity;

namespace TestingDemo
{
    public class BloggingContext : DbContext
    {
        public virtual DbSet<Blog> Blogs { get; set; }
        public virtual 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; }
    }
}

Propriedades Virtuais de DbSet com o Designer EF

Observe que as propriedades DbSet no contexto são marcadas como virtuais. Isso permitirá que a estrutura de simulação derive de nosso contexto e substitua essas propriedades por uma implementação simulada.

Se você estiver usando o Code First, poderá editar suas classes diretamente. Se você estiver usando o Designer EF, precisará editar o modelo T4 que gera seu contexto. Abra o <arquivo model_name.Context.tt> aninhado no arquivo edmx, localize o seguinte fragmento de código e adicione a palavra-chave virtual, conforme mostrado.

public string DbSet(EntitySet entitySet)
{
    return string.Format(
        CultureInfo.InvariantCulture,
        "{0} virtual DbSet\<{1}> {2} {{ get; set; }}",
        Accessibility.ForReadOnlyProperty(entitySet),
        _typeMapper.GetTypeName(entitySet.ElementType),
        _code.Escape(entitySet));
}

Serviço a ser testado

Para demonstrar testes com duplas de teste na memória, escreveremos alguns testes para um BlogService. O serviço é capaz de criar novos blogs (AddBlog) e retornar todos os Blogs ordenados pelo nome (GetAllBlogs). Além do GetAllBlogs, também fornecemos um método que obterá de forma assíncrona todos os blogs ordenados pelo nome (GetAllBlogsAsync).

using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;

namespace TestingDemo
{
    public class BlogService
    {
        private BloggingContext _context;

        public BlogService(BloggingContext context)
        {
            _context = context;
        }

        public Blog AddBlog(string name, string url)
        {
            var blog = _context.Blogs.Add(new Blog { Name = name, Url = url });
            _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();
        }
    }
}

Testando cenários que não são de consulta

Isso é tudo o que precisamos fazer para começar a testar métodos que não são de consulta. O teste a seguir usa o Moq para criar um contexto. Em seguida, ele cria um DbSet<Blog> e o conecta para ser retornado da propriedade Blogs do contexto. Em seguida, o contexto é usado para criar um novo BlogService que, em seguida, é usado para criar um novo blog usando o método AddBlog. Por fim, o teste verifica se o serviço adicionou um novo Blog e chamou SaveChanges no contexto.

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System.Data.Entity;

namespace TestingDemo
{
    [TestClass]
    public class NonQueryTests
    {
        [TestMethod]
        public void CreateBlog_saves_a_blog_via_context()
        {
            var mockSet = new Mock<DbSet<Blog>>();

            var mockContext = new Mock<BloggingContext>();
            mockContext.Setup(m => m.Blogs).Returns(mockSet.Object);

            var service = new BlogService(mockContext.Object);
            service.AddBlog("ADO.NET Blog", "http://blogs.msdn.com/adonet");

            mockSet.Verify(m => m.Add(It.IsAny<Blog>()), Times.Once());
            mockContext.Verify(m => m.SaveChanges(), Times.Once());
        }
    }
}

Cenários de teste de consulta

Para poder executar consultas em nosso teste de DbSet duplo, precisamos configurar uma implementação do IQueryable. A primeira etapa é criar alguns dados na memória – estamos usando uma Lista<Blog>. Em seguida, criamos um contexto e o DBSet, e então ligamos a implementação de IQueryable ao DbSet – eles estão simplesmente delegando ao provedor LINQ to Objects que trabalha com a List.

Em seguida, podemos criar um BlogService com base nos nossos dublês de teste e garantir que os dados que obtemos de GetAllBlogs estejam ordenados por nome.

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;

namespace TestingDemo
{
    [TestClass]
    public class QueryTests
    {
        [TestMethod]
        public void GetAllBlogs_orders_by_name()
        {
            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);

            var service = new BlogService(mockContext.Object);
            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);
        }
    }
}

Teste com consultas assíncronas

O Entity Framework 6 introduziu um conjunto de métodos de extensão que podem ser usados para executar uma consulta de forma assíncrona. Exemplos desses métodos incluem ToListAsync, FirstAsync, ForEachAsync etc.

Como as consultas do Entity Framework usam LINQ, os métodos de extensão são definidos em IQueryable e IEnumerable. No entanto, como eles são projetados apenas para serem usados com o Entity Framework, você poderá receber o seguinte erro se tentar usá-los em uma consulta LINQ que não seja uma consulta do Entity Framework:

O IQueryable de origem não implementa IDbAsyncEnumerable{0}. Somente fontes que implementam IDbAsyncEnumerable podem ser usadas para operações assíncronas do Entity Framework. Para obter mais detalhes, consulte http://go.microsoft.com/fwlink/?LinkId=287068.

Embora os métodos assíncronos só sejam suportados ao rodar em uma consulta do Entity Framework, talvez você queira utilizá-los no teste de unidade ao executar uma simulação em memória de um DbSet.

Para usar os métodos assíncronos, precisamos criar um DbAsyncQueryProvider na memória para processar a consulta assíncrona. Embora seja possível configurar um provedor de consultas usando o Moq, é muito mais fácil criar uma implementação dupla de teste no código. O código para essa implementação é o seguinte:

using System.Collections.Generic;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;

namespace TestingDemo
{
    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; }
        }
    }
}

Agora que temos um provedor de consulta assíncrono, podemos escrever um teste de unidade para nosso novo método GetAllBlogsAsync.

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Threading.Tasks;

namespace TestingDemo
{
    [TestClass]
    public class AsyncQueryTests
    {
        [TestMethod]
        public async Task GetAllBlogsAsync_orders_by_name()
        {

            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<IDbAsyncEnumerable<Blog>>()
                .Setup(m => m.GetAsyncEnumerator())
                .Returns(new TestDbAsyncEnumerator<Blog>(data.GetEnumerator()));

            mockSet.As<IQueryable<Blog>>()
                .Setup(m => m.Provider)
                .Returns(new TestDbAsyncQueryProvider<Blog>(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);

            var service = new BlogService(mockContext.Object);
            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);
        }
    }
}