Partilhar via


Testar com os próprios dublês de teste

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 gravar testes para seu aplicativo, geralmente é desejável evitar atingir o banco de dados. O Entity Framework permite fazer isso ao criar um contexto, com o comportamento definido por seus testes, que usa dados na memória.

Opções na criação de dublês de teste

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

  • Criar as próprios dublês de teste – essa abordagem envolve gravar na memória a própria implementação de contexto e DbSets. Isso oferece bastante controle sobre como as classes se comportam, mas podem envolver a gravação e propriedade de uma quantidade razoável de código.
  • Usar uma estrutura de simulação para criar dublês de teste – usando uma estrutura de simulação (como o Moq), você pode obter implementações de contexto na memória e conjuntos criados dinamicamente em runtime para você.

Este artigo lidará com a criação do seu dublê de teste. Para obter informações sobre como usar uma estrutura de simulação, confira Testar com uma estrutura de simulação.

Testar com versões anteriores do EF6

O código mostrado neste artigo é compatível com o EF6. Para testar com o EF5 e versões anteriores, confira Testar com um contexto falso.

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

Os dublês de teste na memória podem ser uma boa maneira de fornecer cobertura de nível de teste de unidade de bits de aplicativo que usam o EF. No entanto, com isso você estará usando o LINQ to Objects para executar consultas em dados na memória. Isso pode resultar em comportamento diferente do que usar o provedor LINQ (LINQ to Entities) do EF para converter consultas para SQL que são executadas no seu banco de dados.

Um exemplo dessa diferença é o carregamento de dados relacionados. Se você criar uma série de blogs, e cada um tiver postagens relacionadas, quando você 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 ao 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 listas de código completas e você pode copiá-las no Visual Studio para acompanhar, se desejar. Fica mais fácil de criar um Projeto de teste de unidade e será necessário direcionar o .NET Framework 4.5 para concluir as seções que usam assíncrono.

Criar uma interface de contexto

Vamos conferir o teste de um serviço que usa um modelo do EF. Para poder substituir nosso contexto do EF por uma versão para teste na memória, definiremos uma interface que será implementada por nosso contexto do EF (e seu dublê na memória).

O serviço que vamos testar consulta e modifica os dados usando as propriedades DbSet do nosso contexto e, além disso, chama SaveChanges para enviar alterações por push ao banco de dados. Portanto, esses membros serão incluídos na interface.

using System.Data.Entity;

namespace TestingDemo
{
    public interface IBloggingContext
    {
        DbSet<Blog> Blogs { get; }
        DbSet<Post> Posts { get; }
        int SaveChanges();
    }
}

O modelo do EF

O serviço que vamos testar usa um modelo do EF composto por BloggingContext e pelas classes blog e post. Esse código pode ter sido gerado pelo Designer do EF ou ser um modelo do 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; }
    }
}

Implementar a interface de contexto com o Designer do EF

Observe que nosso contexto implementa a interface IBloggingContext.

Se estiver usando o Code First, você poderá editar seu contexto diretamente para implementar a interface. Se estiver usando o Designer do EF, você precisará editar o modelo T4 que gera seu contexto. Abra o arquivo <model_name>.Context.tt que está aninhado no arquivo edmx, encontre o fragmento de código a seguir e adicione a interface, conforme mostrado.

<#=Accessibility.ForType(container)#> partial class <#=code.Escape(container)#> : DbContext, IBloggingContext

Serviço a ser testado

Para demonstrar testes com dublês de teste na memória, gravaremos alguns testes para um BlogService. O serviço é capaz de criar novos blogs (AddBlog) e retornar todos os Blogs classificados por nome (GetAllBlogs). Além do GetAllBlogs, também fornecemos um método que obterá de forma assíncrona todos os blogs classificados por nome (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();
        }
    }
}

Criar os dublês de teste na memória

Agora que temos o modelo do EF real e o serviço que pode usá-lo, é o momento de criar dublê de teste na memória que podemos usar para teste. Criamos um dublê de teste de TestContext para nosso contexto. Em dublês de teste, podemos escolher o comportamento desejado para dar suporte aos testes que vamos executar. Neste exemplo, estamos apenas capturando o número de vezes que SaveChanges é chamado, mas é possível incluir toda lógica necessária para verificar o cenário que você está testando.

Também criamos um TestDbSet que fornece uma implementação na memória do DbSet. Fornecemos uma implementação completa para todos os métodos em DbSet (exceto Find), mas você só precisa implementar os membros que seu cenário de teste usará.

TestDbSet usa algumas outras classes de infraestrutura que incluímos para garantir que as consultas assíncronas possam ser processadas.

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; }
        }
    }
}

Implementação de Find

O método Find é difícil de implementar de forma genérica. Se for necessário testar o código que usa o método Find, é mais fácil criar um DbSet de teste para cada um dos tipos de entidade que precisam dar suporte ao Find. Em seguida, você pode gravar a lógica para encontrar esse tipo específico de entidade, conforme mostrado abaixo.

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);
        }
    }
}

Gravar alguns testes

É tudo o que precisamos fazer para começar a testar. O teste a seguir cria um TestContext e, em seguida, um serviço com base nesse contexto. Em seguida, o serviço é usado para criar um novo blog usando o método AddBlog. Por fim, o teste verifica se o serviço adicionou um novo blog à propriedade Blogs do contexto e chamou SaveChanges no contexto.

Este é apenas um exemplo dos tipos de coisas que você pode testar com um dublê de teste na memória e você pode ajustar a lógica dos dublês de teste e a verificação para atender aos seus 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);
        }
    }
}

Confira outro exemplo de um teste, desta vez um que executa uma consulta. O teste começa criando um contexto de teste com alguns dados em sua propriedade Blog. Observe, os dados não estão em ordem alfabética. Em seguida, podemos criar um BlogService com base em nosso contexto de teste e garantir que os dados que obtemos de GetAllBlogs sejam classificados por nome.

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 fim, gravaremos mais um teste usando nosso método assíncrono para garantir que a infraestrutura assíncrona incluída em TestDbSet estará funcionando.

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);
        }
    }
}