Partilhar via


Testando com uma estrutura de simulação

Observação

EF6 em diante apenas: os recursos, as APIs etc. discutidos nessa página foram introduzidos no 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 escrever sua própria implementação na memória de seu 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.

Teste 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 duplos de teste na memória do EF

Os duplos de teste na memória podem ser uma boa maneira de fornecer cobertura de nível de teste de unidade de bits de seu aplicativo que usam 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.

Acompanhando este 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á direcionar o .NET Framework 4.5 para concluir as seções que usam assíncrono.

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 de DbSet Virtual com Designer EF

Observe que as propriedades DbSet no contexto são marcadas como virtuais. Isso permitirá que a estrutura de simulação derivar de nosso contexto e substituir essas propriedades com 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 <Blog>DbSet 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 consulta de teste

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 um List<Blog>. Em seguida, criamos um contexto e o DBSet<Blog> e, em seguida, conectamos a implementação IQueryable para o DbSet – eles estão apenas delegando para o provedor LINQ to Objects que funciona com a List<T>.

Em seguida, podemos criar um BlogService com base em nossos duplos de teste e garantir que os dados que obtemos de GetAllBlogs sejam ordenados pelo 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 informações, confira http://go.microsoft.com/fwlink/?LinkId=287068.

Embora os métodos assíncronos só sejam suportados durante a execução em uma consulta EF, talvez você queira usá-los no teste de unidade ao executar em um teste na memória duplo 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);
        }
    }
}