实体框架4.1的假DbContext测试

时间:2011-08-01 20:36:33

标签: .net asp.net-mvc unit-testing entity-framework tdd

我正在使用本教程伪造我的DbContext并测试:http://refactorthis.wordpress.com/2011/05/31/mock-faking-dbcontext-in-entity-framework-4-1-with-a-generic-repository/

但我必须更改FakeMainModuleContext实现才能在我的控制器中使用:

public class FakeQuestiona2011Context : IQuestiona2011Context
{
    private IDbSet<Credencial> _credencial;
    private IDbSet<Perfil> _perfil;
    private IDbSet<Apurador> _apurador;
    private IDbSet<Entrevistado> _entrevistado;
    private IDbSet<Setor> _setor;
    private IDbSet<Secretaria> _secretaria;
    private IDbSet<Pesquisa> _pesquisa;
    private IDbSet<Pergunta> _pergunta;
    private IDbSet<Resposta> _resposta;

    public IDbSet<Credencial> Credencial { get { return _credencial ?? (_credencial = new FakeDbSet<Credencial>()); } set { } }
    public IDbSet<Perfil> Perfil { get { return _perfil ?? (_perfil = new FakeDbSet<Perfil>()); } set { } }
    public IDbSet<Apurador> Apurador { get { return _apurador ?? (_apurador = new FakeDbSet<Apurador>()); } set { } }
    public IDbSet<Entrevistado> Entrevistado { get { return _entrevistado ?? (_entrevistado = new FakeDbSet<Entrevistado>()); } set { } }
    public IDbSet<Setor> Setor { get { return _setor ?? (_setor = new FakeDbSet<Setor>()); } set { } }
    public IDbSet<Secretaria> Secretaria { get { return _secretaria ?? (_secretaria = new FakeDbSet<Secretaria>()); } set { } }
    public IDbSet<Pesquisa> Pesquisa { get { return _pesquisa ?? (_pesquisa = new FakeDbSet<Pesquisa>()); } set { } }
    public IDbSet<Pergunta> Pergunta { get { return _pergunta ?? (_pergunta = new FakeDbSet<Pergunta>()); } set { } }
    public IDbSet<Resposta> Resposta { get { return _resposta ?? (_resposta = new FakeDbSet<Resposta>()); } set { } }

    public void SaveChanges()
    {
        // do nothing (probably set a variable as saved for testing)
    }
}

我的测试就是这样:

[TestMethod]
public void IndexTest()
{
    IQuestiona2011Context fakeContext = new FakeQuestiona2011Context();
    var mockAuthenticationService = new Mock<IAuthenticationService>();

    var apuradores = new List<Apurador>
    {
        new Apurador() { Matricula = "1234", Nome = "Acaz Souza Pereira", Email = "acaz@telecom.inf.br", Ramal = "1234" },
        new Apurador() { Matricula = "4321", Nome = "Samla Souza Pereira", Email = "samla@telecom.inf.br", Ramal = "4321" },
        new Apurador() { Matricula = "4213", Nome = "Valderli Souza Pereira", Email = "valderli@telecom.inf.br", Ramal = "4213" }
    };
    apuradores.ForEach(apurador => fakeContext.Apurador.Add(apurador));

    ApuradorController apuradorController = new ApuradorController(fakeContext, mockAuthenticationService.Object);
    ActionResult actionResult = apuradorController.Index();

    Assert.IsNotNull(actionResult);
    Assert.IsInstanceOfType(actionResult, typeof(ViewResult));

    ViewResult viewResult = (ViewResult)actionResult;

    Assert.IsInstanceOfType(viewResult.ViewData.Model, typeof(IndexViewModel));

    IndexViewModel indexViewModel = (IndexViewModel)viewResult.ViewData.Model;

    Assert.AreEqual(3, indexViewModel.Apuradores.Count);
}

我做得对吗?

5 个答案:

答案 0 :(得分:121)

不幸的是,你做得不对,因为那篇文章是错误的。它假装FakeContext会使您的代码单元可测试但不会。一旦将IDbSetIQueryable暴露给您的控制器并且您在内存集合中伪造了该集合,您就永远无法确定您的单元测试是否真正测试了您的代码。在控制器中编写LINQ查询非常容易,它将通过单元测试(因为FakeContext使用LINQ-to-Objects)但在运行时失败(因为您的真实上下文使用LINQ-to-Entities)。这使得您的单元测试的整个目的毫无用处。

我的观点:如果你想将集合暴露给控制器,不要打扰伪造上下文。而是使用集成测试与真实数据库进行测试。这是如何验证控制器中定义的LINQ查询执行预期的唯一方法。

当然,如果你只想在你的套装上拨打ToListFirstOrDefaultFakeContext会很好地为你服务,但是一旦你做了更复杂的事情,你很快就会找到一个陷阱(只需将字符串“无法翻译成商店表达式”添加到Google中 - 所有这些问题只会在您运行Linq-to-entities时出现,但它们会通过Linq-to-objects传递您的测试)

这是一个非常常见的问题,因此您可以查看其他一些示例:

答案 1 :(得分:62)

  

“不幸的是你做得不对,因为那篇文章是错误的。它假装FakeContext会使你的代码单元可测试但不会”

我是您所引用的博客文章的创建者。我在这里看到的问题是对N层单元测试的基本原理的误解。我的帖子并不是要直接用于测试控制器逻辑。

单元测试应该完全按照名称暗示并测试'One Unit'。如果我正在测试一个控制器(正如你上面所做的那样),我会忘记所有关于数据访问的事情。我应该在脑海中删除所有对数据库上下文的调用,并用黑盒方法调用替换它们,好像这些操作对我来说是未知的。这是我有兴趣测试的那些操作的代码。

示例:

在我的MVC应用程序中,我们使用存储库模式。我有一个存储库,比如CustomerRepository:ICustomerRepository,它将执行我的所有客户数据库操作。

如果我要测试我的控制器,我是否希望测试测试我的存储库,数据库访问和控制器逻辑本身?当然不是! 这个管道中有很多'单位'。你想要做的是创建一个虚假的存储库,它实现了ICustomerRepository,允许你单独测试控制器逻辑。

据我所知,仅凭数据库上下文无法完成。 (除了使用Microsoft Moles之外,您可以查看是否需要)。这只是因为所有查询都在控制器类的上下文之外执行。

如果我想测试CustomerRepository逻辑,我该怎么做?最简单的方法是使用伪上下文。这将允许我确保当我试图通过id获得客户时,它实际上通过id获取客户等等。存储库方法非常简单,“无法转换为存储表达式”问题通常不会浮出水面。虽然在某些小的情况下它可能(有时由于错误编写的linq查询)在这些情况下,执行集成测试也很重要,这些测试将一直测试代码到数据库。这些问题将在集成测试中找到。我已经使用这种N层技术已经有一段时间了,并且发现没有问题。

整合测试

显然,针对数据库本身测试您的应用程序是一项代价高昂的工作,一旦您获得成千上万的测试就变成了一场噩梦,另一方面,它最能模仿代码在“现实世界”中的使用方式。这些测试也很重要(从ui到数据库),它们将作为集成测试的一部分执行,而不是单元测试。

Acaz,您在场景中真正需要的是一个可模拟/可伪造的存储库。如果您希望像操作那样测试控制器,那么您的控制器应该接收一个包装数据库功能的对象。然后它可以返回您需要的任何内容,以便测试控制器功能的所有方面。

请参阅http://msdn.microsoft.com/en-us/library/ff714955.aspx

为了测试存储库本身(如果在所有情况下都需要进行辩论),您将要么伪造上下文或使用“Moles”框架中的某些内容。

LINQ本质上很难测试。使用扩展方法在上下文之外定义查询的事实为我们提供了很大的灵活性,但却产生了测试的噩梦。将您的上下文包装在存储库中,这个问题就会消失。

抱歉这么久了:)

答案 2 :(得分:23)

正如Ladislav Mrnka所提到的,你应该测试Linq-to-Entity而不是Linq-to-Object。我通常使用Sql CE作为测试数据库,并在每次测试之前始终重新创建数据库。这可能会使测试有点慢,但到目前为止,我对100多个单元测试的性能表现不错。

首先,在测试项目的 App.config 中使用SqlCe更改连接字符串设置。

<connectionStrings>
    <add name="MyDbContext"
       connectionString="Data Source=|DataDirectory|MyDb.sdf"
         providerName="System.Data.SqlServerCe.4.0"
         />
</connectionStrings>

其次,将db初始化程序设置为DropCreateDatabaseAlways

Database.SetInitializer<MyDbContext>(new DropCreateDatabaseAlways<MyDbContext>());

然后,在运行每个测试之前强制EF初始化。

public void Setup() {
    Database.SetInitializer<MyDbContext>(new DropCreateDatabaseAlways<MyDbContext>());

    context = new MyDbContext();
    context.Database.Initialize(force: true);
}

如果您使用的是xunit,请在构造函数中调用Setup方法。如果您正在使用MSTest,请在该方法上放置TestInitializeAttribute。如果是nunit .......

答案 3 :(得分:1)

您可以使用Effort for EF 6+创建一个Fake DbContext。见https://effort.codeplex.com/。努力代表 E ntity F ramework F ake O bjectContext R ealization Ť OOL。

对于包含工作样本的文章,请参阅http://www.codeproject.com/Tips/1036630/Using-Effort-Entity-Framework-Unit-Testing-Toolhttp://www.codeproject.com/Articles/460175/Two-strategies-for-testing-Entity-Framework-Effort?msg=5122027#xx5122027xx

答案 4 :(得分:0)

我知道我们不应该这样做,但有时候你必须这样做(例如你的老板也可能会问你,不会改变主意)。

所以我必须这样做,我把它放在这里可能对某些人有所帮助。我对c#/ .net很新,所以我认为它远非优化/清洁,但它似乎有效。

在MSDN Find here the missing class之后使用一些反射,我设法添加了单向属性:这里的关键元素是 AddNavigationProperty RefreshNavigationProperties 。如果有人建议改进这些代码,我很乐意接受它们

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Linq.Expressions;

namespace MockFactory
{
    public class TestDbSet<TEntity> : DbSet<TEntity>, IQueryable, IEnumerable<TEntity>, IDbAsyncEnumerable<TEntity>
        where TEntity : class
    {
        public readonly ObservableCollection<TEntity> _data;
        private readonly IQueryable _query;
        private readonly Dictionary<Type, object> entities;

        public TestDbSet()
        {
            _data = new ObservableCollection<TEntity>();
            _query = _data.AsQueryable();

            entities = new Dictionary<Type, object>();
        }

        public override ObservableCollection<TEntity> Local
        {
            get { return _data; }
        }

        IDbAsyncEnumerator<TEntity> IDbAsyncEnumerable<TEntity>.GetAsyncEnumerator()
        {
            return new TestDbAsyncEnumerator<TEntity>(_data.GetEnumerator());
        }

        IEnumerator<TEntity> IEnumerable<TEntity>.GetEnumerator()
        {
            return _data.GetEnumerator();
        }

        Type IQueryable.ElementType
        {
            get { return _query.ElementType; }
        }

        Expression IQueryable.Expression
        {
            get { return _query.Expression; }
        }

        IQueryProvider IQueryable.Provider
        {
            get { return new TestDbAsyncQueryProvider<TEntity>(_query.Provider); }
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return _data.GetEnumerator();
        }

        public void AddNavigationProperty<T>(DbSet<T> dbSet) where T : class
        {
            entities.Add(typeof (T), dbSet);
        }

        public void RefreshNavigationProperty(TEntity item)
        {
            foreach (var entity in entities)
            {
                var property = item.GetType().GetProperty(entity.Key.Name);

                var type =
                    (int)item.GetType().GetProperty(entity.Key.Name.Replace(typeof(TEntity).Name, "")).GetValue(item);

                var dbSets = (IEnumerable<object>)entity.Value.GetType().GetField("_data").GetValue(entity.Value);

                var dbSet = dbSets.Single(x => (int)x.GetType().GetProperty("Id").GetValue(x) == type);
                property.SetValue(item, dbSet);
            }
        }

        public override TEntity Add(TEntity item)
        {
            RefreshNavigationProperty(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 TestContext()
        {
            TypeUsers = new TestDbSet<TypeUser>();
            StatusUsers = new TestDbSet<StatusUser>();

            TypeUsers.Add(new TypeUser {Description = "FI", Id = 1});
            TypeUsers.Add(new TypeUser {Description = "HR", Id = 2});

            StatusUsers.Add(new StatusUser { Description = "Created", Id = 1 });
            StatusUsers.Add(new StatusUser { Description = "Deleted", Id = 2 });
            StatusUsers.Add(new StatusUser { Description = "PendingHR", Id = 3 });


            Users = new TestDbSet<User>();

            ((TestDbSet<User>) Users).AddNavigationProperty(StatusUsers);
           ((TestDbSet<User>)Users).AddNavigationProperty(TypeUsers);

        }

        public override DbSet<TypeUser> TypeUsers { get; set; }
        public override DbSet<StatusUser> StatusUsers { get; set; }
        public override DbSet<User> Users { get; set; }
        public int SaveChangesCount { get; private set; }

        public override int SaveChanges(string modifierId)
        {
            SaveChangesCount++;
            return 1;
        }
    }

最后在测试中不要忘记在执行断言之前刷新导航属性(应该有更好的方法,但我找不到它)

ContextFactory.Entity.Users.Each(((TestDbSet<User>) ContextFactory.Entity.Users).RefreshNavigationProperty);