65.9K
CodeProject 正在变化。 阅读更多。
Home

去存储库还是不去

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (31投票s)

2015年2月11日

CPOL

11分钟阅读

viewsIcon

72329

我们将探讨如何单元测试使用 Repository 的代码,而不是直接使用 Entity Framework,我们将通过模拟/测试替身来实现。

引言

简而言之

本文演示了如何单元测试使用 Repository 或直接使用 Entity Framework 的代码。

详而言之

在工作中,我们使用多种技术来处理数据层代码,包括原始 ADO .NET / Dapper 和 NHibernate。我使用 NHibernate 大约两年了,但以前我用过 Entity Framework。与许多人一样,我开发了一些模式来更轻松地测试我的数据访问代码。这通常意味着使用 Repository 模式,它充当实际数据库的抽象。使用 Repository 模式非常棒,绝对能让你创建模拟/测试替身,从而无需依赖数据库即可测试所有代码。

我喜欢使用 Repository 模式的另一个方面是,我喜欢传入一个 Unit Of Work 抽象,以便可以将多个 Repository 操作组合在一个提交中。在 Nhibernate 中,这将是 ISession 抽象。在使用 Entity Framework 时,没有通用的接口抽象来表示 Unit Of Work,但我们可以完全自由地创建自己的接口,并让我们的 Entity Framework DbContext 实现该接口。

言归正传,我的观点是,我通常使用 Repository 模式来提高代码的可测试性。问题是,前几天我偶然看到了 Entity Framework 团队的一篇博文,他们谈论的是直接使用 DbContext(这也是本文将使用的),因为他们做了大量工作来使其更具可测试性。这篇博文来自 Entity Framework 团队,他们讨论的是当前(撰写本文时是 EF 6)版本的 Entity Framework,本文也基于此。

这不是我第一次看到博文告诉我们要放弃 Repository,事实上,这里还有 Ayende 的几篇博文,他是 NHibernate 的主要推动者之一。

http://ayende.com/blog/3955/repository-is-the-new-singleton
http://ayende.com/blog/4784/architecting-in-the-pit-of-doom-the-evils-of-the-repository-abstraction-layer

嗯,很有意思。

正如我在工作中提到的,我使用 NHibernate(我也使用 Repository 来辅助我的测试,尽管我不担心创建规范类,因为有了 IQueryable、表达式树和 lambda 表达式,何必去做呢),但我对 Entity Framework 情有独钟,所以本文将使用它。

考虑到以上所有因素,我决定创建两个简单的类来测试以下内容:

  • 第一个类将依赖一个 Repository 来处理所有数据访问。
  • 第二个类将直接使用 Entity Framework。

对于这两种场景,都将有一些代码和一些测试来验证代码在不依赖数据库的情况下工作,使用模拟和测试替身的组合。

 

致歉

由于本文全部是关于测试,因此文本内容不多,因为它主要只是展示待测系统和测试。因此,本文中有大量代码,别无他物,对此我提前表示歉意,希望测试代码能对您有所帮助。

 

 

代码在哪里?

代码可在我的 github 账户上找到: https://github.com/sachabarber/TestingEntityFramework

 

必备组件

为了运行本文相关的代码,您需要以下条件:

  • SQL Server 安装
  • 在新的(或现有的)SQL 数据库上运行以下两个设置脚本。
    • DB Scripts\Create Posts.sql
    • DB Scripts\Create Post Comments.sql
  • 请确保您已修改以下项目中的 App.Config 文件,以反映您的 SQL Server 安装情况:
    • EFTest.csproj
    • EFTest.WithRepositories.csproj

 

使用 Repository 进行测试

  • 参见: EFTest.WithRepositories.csproj
  • 参见: EFTest.WithRepositories.Tests.csproj

本节讨论了一组使用 Repository 模式以及 Unit Of Work 抽象的文件。

 

使用 Repository 的待测系统 (SUT)

这是我们将要测试的类。

public class SomeService : ISomeService, IDisposable
{
    private readonly IUnitOfWork context;
    private readonly IRepository<Post> repository;

    private int counter;

    public SomeService(IUnitOfWork context, IRepository<Post> repository)
    {
        this.context = context;
        this.repository = repository;
    }

    public void Insert(string url)
    {
        Post post = new Post() { Url = url };
        post.PostComments.Add(new PostComment()
        {
            Comment = string.Format("yada yada {0}", counter++)
        });
        repository.Add(post);
    }

    public IEnumerable<Post> GetAll()
    {
        return repository.GetAll();
    }

    public IEnumerable<Post> GetAll(Expression<Func<Post, bool>> filter)
    {
        return repository.GetAll(filter);
    }

    public Post FindById(int id)
    {
        var post = repository.Get(id);
        return post;
    }

    public Task<bool> InsertAsync(string url)
    {
        Post post = new Post() { Url = url };
        post.PostComments.Add(new PostComment()
        {
            Comment = string.Format("yada yada {0}", counter++)
        });
        return repository.AddAsync(post);
    }

    public async Task<List<Post>> GetAllAsync()
    {
        var posts = await repository.GetAllAsync();
        return posts.ToList();
    }

    public Task<Post> FindByIdAsync(int id)
    {
        return repository.GetIncludingAsync(id, x => x.PostComments);

    }

    public void Dispose()
    {
        context.Dispose();
    }
}

可以看到,我们需要测试其中的几个方面,即这些方法:

//Sync
void Insert(string url);
IEnumerable<Post> GetAll();
IEnumerable<Post> GetAll(Expression<Func<Post, bool>> filter);
Post FindById(int id);

//Async
Task<bool> InsertAsync(string url);
Task<List<Post>> GetAllAsync();
Task<Post> FindByIdAsync(int id);

 

既然我们知道了要测试的内容,让我们继续看看其他移动部件。

 

存储库

我倾向于使用这种 Repository。有几点需要注意:

  1. 它是一个通用 Repository,可以针对多个实体类型使用(如果您需要专用 Repository,您可能需要放弃通用 Repository,转而使用简单的继承自 Repository<T> 的特定 Repository)。
  2. 它可以使用 Unit Of Work 抽象。
  3. 它可以用于包含 DbContext 导航属性。
public class Repository<T> : IRepository<T> where T : class, IEntity 
{
    private readonly IUnitOfWork context;

    public Repository(IUnitOfWork context)
    {
        this.context = context;
    }


    #region Sync
    public int Count()
    {
        return context.Get<T>().Count(); 
    }

    public void Add(T item)
    {
        context.Add(item);
    }

    public bool Contains(T item)
    {
        return context.Get<T>().FirstOrDefault(t => t == item) != null;
    }

    public bool Remove(T item)
    {
        return context.Remove(item);
    }

    public T Get(int id)
    {
        return context.Get<T>().SingleOrDefault(x => x.Id == id);
    }

    public T GetIncluding(
        int id, 
        params Expression<Func<T, object>>[] includeProperties)
    {
        return GetAllIncluding(includeProperties).SingleOrDefault(x => x.Id == id);
    }


    public IQueryable<T> GetAll()
    {
        return context.Get<T>();
    }

    public IQueryable<T> GetAll(Expression<Func<T, bool>> predicate)
    {
        return context.Get<T>().Where(predicate).AsQueryable<T>();
    }

    /// <summary>
    /// Used for Lazyloading navigation properties
    /// </summary>
    public IQueryable<T> GetAllIncluding(
        params Expression<Func<T, object>>[] includeProperties)
    {
        IQueryable<T> queryable = GetAll();
        foreach (Expression<Func<T, object>> includeProperty in includeProperties)
        {
            queryable = queryable.Include(includeProperty);
        }
        return queryable;
    }

    #endregion

    #region Async
    public async Task<int> CountAsync()
    {
        return await Task.Run(() => context.Get<T>().Count()); 
    }

    public Task<bool> AddAsync(T item)
    {
        return Task.Run(() =>
            {
                context.Add(item);
                return true;
            });
    }

    public Task<bool> ContainsAsync(T item)
    {
        return Task.Run(
            () => context.Get<T>().FirstOrDefault(t => t == item) != null);
    }

    public Task<bool> RemoveAsync(T item)
    {
        return Task.Run(() => context.Remove(item));
            
    }

    public Task<T> GetAsync(int id)
    {
        return Task.Run(
            () => context.Get<T>().SingleOrDefault(x => x.Id == id));
    }

    public async Task<T> GetIncludingAsync(
        int id, 
        params Expression<Func<T, object>>[] includeProperties)
    {
        IQueryable<T> queryable = await GetAllIncludingAsync(includeProperties);
        return await queryable.SingleOrDefaultAsync(x => x.Id == id);
    }

    public Task<IQueryable<T>> GetAllAsync()
    {
        return Task.Run(() => context.Get<T>());
    }

    public Task<IQueryable<T>> GetAllAsync(
        Expression<Func<T, bool>> predicate)
    {
        return Task.Run(() => 
            context.Get<T>().Where(predicate).AsQueryable<T>());
    }

    /// <summary>
    /// Used for Lazyloading navigation properties
    /// </summary>
    public Task<IQueryable<T>> GetAllIncludingAsync(
        params Expression<Func<T, object>>[] includeProperties)
    {
        return Task.Run(
            () =>
            {
                IQueryable<T> queryable = GetAll();
                foreach (Expression<Func<T, object>> includeProperty in includeProperties)
                {
                    queryable = queryable.Include(includeProperty);
                }
                return queryable;

            });
    }

    #endregion
}

 

工作单元抽象

正如我所说,我上面展示的 Repository 代码依赖于 Unit Of Work 抽象。那么这个抽象到底是什么呢?简单来说,它就是一个 Entity Framework DbContext,只是我们不会直接使用它,而是通过我上面展示的 Repository 来获取/插入数据。正如我之前提到的,让 Repository 使用 Unit Of Work 抽象的一个好处是,我们可以在一个事务中提交多个 Repository 操作。无论如何,这是本示例使用的 Unit Of Work 抽象的代码。

public abstract class EfDataContextBase : DbContext, IUnitOfWork
{

    public EfDataContextBase(string nameOrConnectionString)
        : base(nameOrConnectionString)
    {
    }

    public IQueryable<T> Get<T>() where T : class
    {
        return Set<T>();
    }

    public bool Remove<T>(T item) where T : class
    {
        try
        {
            Set<T>().Remove(item);
        }
        catch (Exception)
        {
            return false;
        }
        return true;
    }

    public new int SaveChanges()
    {
        return base.SaveChanges();
    }

    public void Attach<T>(T obj) where T : class
    {
        Set<T>().Attach(obj);
    }

    public void Add<T>(T obj) where T : class
    {
        Set<T>().Add(obj);

            
    }
}



public class RepositoryExampleSachaTestContext : EfDataContextBase, ISachaContext
{
    public RepositoryExampleSachaTestContext(string nameOrConnectionString)
        : base(nameOrConnectionString)
    {
        this.Configuration.LazyLoadingEnabled = true;
        this.Configuration.ProxyCreationEnabled = false;
    }



    public DbSet<Post> Posts { get; set; }

    public void DoSomethingDirectlyWithDatabase()
    {
        //Not done for this example
    }
}

 

使用 Repository 的 IOC 注入

为了正确地连接所有这些,我使用了一个 IOC 容器。我选择了 Autofac。老实说,IOC 代码有点附带性质,没什么特别的,但我还是为完整性将其包含在内。

public class IOCManager
{
    private static IOCManager instance;

    static IOCManager()
    {
        instance = new IOCManager();
    }

    private IOCManager()
    {
        var builder = new ContainerBuilder();

        // Register individual components
        builder.RegisterType<RepositoryExampleSachaTestContext>()
            .As<IUnitOfWork>()
            .WithParameter("nameOrConnectionString", "SachaTestContextConnection")
            .InstancePerLifetimeScope();

        builder.RegisterType<SomeService>()
            .As<ISomeService>().InstancePerLifetimeScope();

        builder.RegisterGeneric(typeof(Repository<>))
            .As(typeof(IRepository<>))
            .InstancePerLifetimeScope();

        Container = builder.Build();
    }


    public IContainer Container { get; private set; }

    public static IOCManager Instance
    {
        get
        {
            return instance;
        }
    }


}

使用 Repository 的测试

好了,我们已经看到了所有想要测试的系统部分,现在让我们来看一些测试用例。在所有这些测试中,我将使用 moq 模拟库。

使用 Repository 的 Insert()

以下是如何模拟通过 Repository 发生的插入。显然,如果您的代码依赖于插入的 ID,您需要扩展此功能,并可能提供一个 回调来更新添加的 Post 的 ID,如果这对您的代码很重要的话。

这是我们试图模拟的代码。

public void Insert(string url)
{
    Post post = new Post() { Url = url };
    post.PostComments.Add(new PostComment()
    {
        Comment = string.Format("yada yada {0}", counter++)
    });
    repository.Add(post);
}

public Task<bool> InsertAsync(string url)
{
    Post post = new Post() { Url = url };
    post.PostComments.Add(new PostComment()
    {
        Comment = string.Format("yada yada {0}", counter++)
    });
    return repository.AddAsync(post);
}

您可以在下面看到同步和异步版本的测试代码。

[TestCase]
public void TestInsert()
{
    Mock<IUnitOfWork> uowMock = new Mock<IUnitOfWork>();
    Mock<IRepository<Post>> repoMock = new Mock<IRepository<Post>>();

    SomeService service = new SomeService(uowMock.Object, repoMock.Object);
    service.Insert("TestInsert");

    repoMock.Verify(m => m.Add(It.IsAny<Post>()), Times.Once());
}



[TestCase]
public async void TestInsertAsync()
{
    Mock<IUnitOfWork> uowMock = new Mock<IUnitOfWork>();
    Mock<IRepository<Post>> repoMock = new Mock<IRepository<Post>>();
    repoMock.Setup(x => x.AddAsync(It.IsAny<Post>())).Returns(Task.FromResult(true));

    SomeService service = new SomeService(uowMock.Object, repoMock.Object);
    await service.InsertAsync("TestInsertAsync");

    repoMock.Verify(m => m.AddAsync(It.IsAny<Post>()), Times.Once());
}

使用 Repository 的 GetAll()

以下是如何模拟通过 Repository 发生的 GetAll() 调用。可以看到,我们可以简单地返回一些模拟的 Post 对象。

这是我们试图模拟的代码。

public IEnumerable<Post> GetAll()
{
    return repository.GetAll();
}

public async Task<List<Post>> GetAllAsync()
{
    var posts = await repository.GetAllAsync();
    return posts.ToList();
}

您可以在下面看到同步和异步版本。

[TestCase]
public void TestGetAll()
{
    Mock<IUnitOfWork> uowMock = new Mock<IUnitOfWork>();
    Mock<IRepository<Post>> repoMock = new Mock<IRepository<Post>>();

    var posts = Enumerable.Range(0, 5)
        .Select(x => new Post()
        {
            Url = string.Format("www.someurl{0}", x)
        }).ToList();
    repoMock.Setup(x => x.GetAll()).Returns(posts.AsQueryable());

    SomeService service = new SomeService(uowMock.Object, repoMock.Object);
    var retrievedPosts  = service.GetAll();

    repoMock.Verify(m => m.GetAll(), Times.Once());

    CollectionAssert.AreEqual(posts, retrievedPosts);
}


[TestCase]
public async void TestGetAllAsync()
{
    Mock<IUnitOfWork> uowMock = new Mock<IUnitOfWork>();
    Mock<IRepository<Post>> repoMock = new Mock<IRepository<Post>>();

    var posts = Enumerable.Range(0, 5).Select(x => new Post()
    {
        Id = x,
        Url = string.Format("www.someurl{0}", x)
    }).ToList();

    repoMock.Setup(x => x.GetAllAsync()).Returns(Task.FromResult(posts.AsQueryable()));

    SomeService service = new SomeService(uowMock.Object, repoMock.Object);
    var retrievedPosts = await service.GetAllAsync();

    repoMock.Verify(m => m.GetAllAsync(), Times.Once());

    CollectionAssert.AreEqual(posts, retrievedPosts);
}

 

GetAll(),提供 Expression<Func<Post,bool>> 过滤器,使用 Repository

我之前发布的 Repository 代码还允许使用 Expression<Func<Post,bool>> 来对 IQueryable<Post> 应用过滤器。

这是我们试图模拟的代码。

public IEnumerable<Post> GetAll(Expression<Func<Post, bool>> filter)
{
    return repository.GetAll(filter);
}

那么我们如何编写能够实现此功能的测试代码呢?其实并不难,我们只需要巧妙地进行模拟,并确保在进行任何断言之前将过滤器应用于模拟对象,方法如下:

[TestCase]
public void TestGetAllWithLambda()
{
    Mock<IUnitOfWork> uowMock = new Mock<IUnitOfWork>();
    Mock<IRepository<Post>> repoMock = new Mock<IRepository<Post>>();

    var posts = Enumerable.Range(0, 5)
        .Select(x => new Post()
        {
            Url = string.Format("www.someurl{0}", x)
        }).ToList();
    for (int i = 0; i < posts.Count; i++)
    {
        posts[i].PostComments.Add(new PostComment()
                                    {
                                        Comment = string.Format("some test comment {0}", i)
                                    });
    }

    repoMock.Setup(moq => moq.GetAll(It.IsAny<Expression<Func<Post, bool>>>()))
            .Returns((Expression<Func<Post, bool>> predicate) => 
                posts.Where(predicate.Compile()).AsQueryable());

    SomeService service = new SomeService(uowMock.Object, repoMock.Object);

    Func<Post, bool> func = (x) => x.Url == "www.someurl1";
    Expression<Func<Post, bool>> filter = post => func(post);

    var retrievedPosts = service.GetAll(filter);
    CollectionAssert.AreEqual(posts.Where(func), retrievedPosts);
}

F使用 Repository 的 FindById()

以下是如何模拟通过 Repository 发生的 FindById() 调用。可以看到,我们可以简单地返回一个模拟的 Post 对象。

这是我们试图模拟的代码。

public Post FindById(int id)
{
    var post = repository.Get(id);
    return post;
}

public Task<Post> FindByIdAsync(int id)
{
    return repository.GetIncludingAsync(id, x => x.PostComments);

}

您可以在下面看到同步和异步版本。

[TestCase]
public void TestFindById()
{
    Mock<IUnitOfWork> uowMock = new Mock<IUnitOfWork>();
    Mock<IRepository<Post>> repoMock = new Mock<IRepository<Post>>();

    var posts = Enumerable.Range(0, 5).Select(x => new Post()
    {
        Id = x,
        Url = string.Format("www.someurl{0}", x)
    }).ToList();

    for (int i = 0; i < posts.Count; i++)
    {
        posts[i].PostComments.Add(new PostComment()
        {
            Comment = string.Format("some test comment {0}", i)
        });
    }

    repoMock.Setup(moq => moq.Get(It.IsInRange(0, 5, Range.Inclusive)))
        .Returns((int id) => posts.SingleOrDefault(x => x.Id == id));


    SomeService service = new SomeService(uowMock.Object, repoMock.Object);
    var retrievedPost = service.FindById(2);


    Assert.AreEqual(2, retrievedPost.Id);
}



[TestCase]
public async void TestFindByIdAsync()
{
    Mock<IUnitOfWork> uowMock = new Mock<IUnitOfWork>();
    Mock<IRepository<Post>> repoMock = new Mock<IRepository<Post>>();

    var posts = Enumerable.Range(0, 5).Select(x => new Post()
    {
        Id = x,
        Url = string.Format("www.someurl{0}", x)
    }).ToList();

    for (int i = 0; i < posts.Count; i++)
    {
        posts[i].PostComments.Add(new PostComment()
        {
            Comment = string.Format("some test comment {0}", i)
        });
    }

    repoMock.Setup(moq => moq.GetIncludingAsync(
                It.IsInRange(0, 5, Range.Inclusive), 
                new[] { It.IsAny<Expression<Func<Post, object>>>() }))
            .Returns(
                (int id, Expression<Func<Post, object>>[] includes) => 
                    Task.FromResult(posts.SingleOrDefault(x => x.Id == id)));


    SomeService service = new SomeService(uowMock.Object, repoMock.Object);
    var retrievedPost = await service.FindByIdAsync(2);


    Assert.AreEqual(2, retrievedPost.Id);
}

这是证明这一切正常工作的证据。

 

 

使用 Entity Framework 进行测试

  • 参见: EFTest.csproj
  • 参见: EFTest.Tests.csproj

好了,我们已经看到确实可以使用 Repository 来简化数据访问代码的测试。然而,正如我所说,Entity Framework 团队发布了一篇博文(https://msdn.microsoft.com/en-us/data/dn314429)),声称可以直接在代码中使用 Entity Framework DbContext,并且仍然可以轻松使用模拟/测试替身。很自然地,我想尝试一下,所以我们开始吧。

本节讨论了使用 DbContext 抽象(您仍然希望使用抽象,以便能够正确模拟和测试那些讨厌的直接 DbContext.Database 调用)。

延迟加载

Entity Framework 允许您关闭延迟加载。这样做之后,您需要自己 Include 导航属性。实际代码包含延迟加载/非延迟加载的示例,但为简洁起见,我选择仅涵盖非延迟加载版本进行测试,因为我认为从测试角度来看,这更具挑战性,您需要管理导航属性,所以可以说它更有趣。

 

待测系统 (SUT)

这是我们将要测试的类。

public class SomeService : ISomeService, IDisposable
{
    private readonly ISachaContext context;
    private int counter;

    public SomeService(ISachaContext context)
    {
        this.context = context;
    }


    public void Insert(string url)
    {
        Post post = new Post() { Url = url };
        post.PostComments.Add(new PostComment()
        {
            Comment = string.Format("yada yada {0}", counter++)
        });
        context.Posts.Add(post);
    }

    public IEnumerable<Post> GetAll()
    {
        return context.Posts.AsEnumerable();
    }


    public IEnumerable<Post> GetAll(Expression<Func<Post, bool>> filter)
    {
        return context.Posts.Where(filter).AsEnumerable();
    }

    public Post FindById(int id)
    {
        //NOTE : Even if you included a line like the one below it would include 
        //the PostComments, which seems to be NonLazy
        //this is due to the fact that the Post(s) and Comment(s) are already in the Context
        //var post1 = context.Posts.FirstOrDefault(p => p.Id == id);

        //This should show that we are not doing Lazy Loading and DO NEED to use 
        //Include for navigation properties
        var postWithNoCommentsProof = context.Posts.FirstOrDefault();
        var postWithCommentsThanksToInclude = context.Posts
            .Include(x => x.PostComments).FirstOrDefault();

        var post = context.Posts.Where(p => p.Id == id)
            .Include(x => x.PostComments).FirstOrDefault();
        return post;
    }

    public async Task<bool> InsertAsync(string url)
    {
        Post post = new Post() { Url = url };
        post.PostComments.Add(new PostComment()
        {
            Comment = string.Format("yada yada {0}", counter++)
        });
        context.Posts.Add(post);
        return true;
    }

    public async Task<List<Post>> GetAllAsync()
    {
        return await context.Posts.ToListAsync(); 
    }


    public async Task<Post> FindByIdAsync(int id)
    {
        //NOTE : Even if you included a line like the one below it would include 
        //the PostComments, which seems to be NonLazy
        //this is due to the fact that the Post(s) and Comment(s) are already in the Context
        //var post1 = context.Posts.FirstOrDefault(p => p.Id == id);

        //This should show that we are not doing Lazy Loading and DO NEED to use 
        //Include for navigation properties
        var postWithNoCommentsProof = await context.Posts.FirstOrDefaultAsync();
        var postWithCommentsThanksToInclude = await context.Posts
            .Include(x => x.PostComments).FirstOrDefaultAsync();

        var post = await context.Posts.Where(p => p.Id == id)
            .Include(x => x.PostComments).FirstOrDefaultAsync();
        return post;
    }

    public void Dispose()
    {
        context.Dispose();
    }
}

IOC 注入

为了正确地连接所有这些,我使用了一个 IOC 容器。我选择了 Autofac。老实说,IOC 代码有点附带性质,没什么特别的,但我还是为完整性将其包含在内。

public class IOCManager
{
    private static IOCManager instance;

    static IOCManager()
    {
        instance = new IOCManager();
    }

    private IOCManager()
    {
        var builder = new ContainerBuilder();
 
        // Register individual components
        builder.RegisterType<SachaContext>()
            .As<ISachaContext>()
            .WithParameter("nameOrConnectionString", "SachaTestContextConnection")
            .InstancePerLifetimeScope();

        builder.RegisterType<SachaLazyContext>()
            .As<ISachaLazyContext>()
            .WithParameter("nameOrConnectionString", "SachaTestContextConnection")
            .InstancePerLifetimeScope();

        builder.RegisterType<SomeService>()
            .As<ISomeService>().InstancePerLifetimeScope();

        builder.RegisterType<SomeServiceLazy>()
            .As<ISomeServiceLazy>().InstancePerLifetimeScope();

        Container = builder.Build();
    }


    public IContainer Container { get; private set; }

    public static  IOCManager Instance
    {
        get
        {
            return instance;
        }
    }
}

测试

好了,我们已经看到了所有想要测试的系统部分,现在让我们来看一些测试用例。在所有这些测试中,我将使用 moq 模拟库。

 

DbContext 测试替身

为了尝试 Entity Framework 博客上的建议,我们需要确保使用 DbContext 的测试替身,以便为其提供模拟/测试替身的 DbSet。这是本文使用的。

public class SachaContextTestDouble : DbContext, ISachaContext
{
    public virtual DbSet<Post> Posts { get; set; }
    public void DoSomethingDirectlyWithDatabase()
}

 

异步版本

下面显示的直接 Entity Framework 代码的异步版本使用了助手类,如 Entity Framework 博客 https://msdn.microsoft.com/en-us/data/dn314429 所述,我在此处显示是为了完整性。

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

namespace EFTest.Tests
{
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; }
    }
}

 

 

 

Insert()

以下是如何模拟通过直接 Entity Framework 使用发生的插入。显然,如果您的代码依赖于插入的 ID,您需要扩展此功能,并可能提供一个 回调来更新添加的 Post 的 ID,如果这对您的代码很重要。

这是我们试图模拟的代码。

public void Insert(string url)
{
    Post post = new Post() { Url = url };
    post.PostComments.Add(new PostComment()
    {
        Comment = string.Format("yada yada {0}", counter++)
    });
    context.Posts.Add(post);
}


public async Task<bool> InsertAsync(string url)
{
    Post post = new Post() { Url = url };
    post.PostComments.Add(new PostComment()
    {
        Comment = string.Format("yada yada {0}", counter++)
    });
    context.Posts.Add(post);
    return true;
}

您可以在下面看到同步和异步版本。

private static Mock<DbSet<T>> CreateMockSet<T>(IQueryable<T> dataForDbSet) where T : class
{
    var dbsetMock = new Mock<DbSet<T>>();

    dbsetMock.As<IQueryable<T>>().Setup(m => m.Provider)
    	.Returns(dataForDbSet.Provider);
    dbsetMock.As<IQueryable<T>>().Setup(m => m.Expression)
    	.Returns(dataForDbSet.Expression);
    dbsetMock.As<IQueryable<T>>().Setup(m => m.ElementType)
    	.Returns(dataForDbSet.ElementType);
    dbsetMock.As<IQueryable<T>>().Setup(m => m.GetEnumerator())
    	.Returns(dataForDbSet.GetEnumerator());
    return dbsetMock;
}


[TestCase]
public void TestInsert()
{
    var dbsetMock = new Mock<DbSet<Post>>();
    var uowMock = new Mock<SachaContextTestDouble>();
    uowMock.Setup(m => m.Posts).Returns(dbsetMock.Object); 

    var service = new SomeService(uowMock.Object);
    service.Insert("Some url");

    dbsetMock.Verify(m => m.Add(It.IsAny<Post>()), Times.Once()); 
}

注意:可以看到,对于 Entity Framework 的模拟,我们需要创建一个模拟的 DbSet,这将在所有示例中都用到。我们使用了一个小技巧,即使用标准的 LINQ to objects 表达式树和 LINQ 提供程序。

GetAll()

以下是如何模拟通过直接 Entity Framework 使用发生的 GetAll() 调用。可以看到,我们可以简单地返回一些模拟的 Post 对象。

这是我们试图模拟的代码。

public IEnumerable<Post> GetAll()
{
    return context.Posts.AsEnumerable();
}

public async Task<List<Post>> GetAllAsync()
{
    return await context.Posts.ToListAsync(); 
}

您可以在下面看到同步和异步版本。

private static Mock<DbSet<T>> CreateMockSet<T>(IQueryable<T> dataForDbSet) where T : class
{
    var dbsetMock = new Mock<DbSet<T>>();

    dbsetMock.As<IQueryable<T>>().Setup(m => m.Provider)
    	.Returns(dataForDbSet.Provider);
    dbsetMock.As<IQueryable<T>>().Setup(m => m.Expression)
    	.Returns(dataForDbSet.Expression);
    dbsetMock.As<IQueryable<T>>().Setup(m => m.ElementType)
    	.Returns(dataForDbSet.ElementType);
    dbsetMock.As<IQueryable<T>>().Setup(m => m.GetEnumerator())
    	.Returns(dataForDbSet.GetEnumerator());
    return dbsetMock;
}



[TestCase]
public void TestGetAll()
{

    var posts = Enumerable.Range(0, 5).Select(
        x => new Post()
        {
            Url = string.Format("www.someurl{0}", x)
        }).AsQueryable();


    var dbsetMock = CreateMockSet(posts);

    var mockContext = new Mock<SachaContextTestDouble>();
    mockContext.Setup(c => c.Posts).Returns(dbsetMock.Object);

    var service = new SomeService(mockContext.Object);
    var retrievedPosts = service.GetAll().ToList();

    var postsList = posts.ToList();

    Assert.AreEqual(posts.Count(), retrievedPosts.Count());
    Assert.AreEqual(postsList[0].Url, retrievedPosts[0].Url);
    Assert.AreEqual(postsList[4].Url, retrievedPosts[4].Url);
}


[TestCase]
public async Task TestGetAllAsync()
{

    var posts = Enumerable.Range(0, 5).Select(
        x => new Post()
        {
            Url = string.Format("www.someurl{0}", x)
        }).AsQueryable();


    var dbsetMock = new Mock<DbSet<Post>>();
    dbsetMock.As<IDbAsyncEnumerable<Post>>()
        .Setup(m => m.GetAsyncEnumerator())
        .Returns(new TestDbAsyncEnumerator<Post>(posts.GetEnumerator()));

    dbsetMock.As<IQueryable<Post>>()
        .Setup(m => m.Provider)
        .Returns(new TestDbAsyncQueryProvider<Post>(posts.Provider));

    dbsetMock.As<IQueryable<Post>>().Setup(m => m.Expression).Returns(posts.Expression);
    dbsetMock.As<IQueryable<Post>>().Setup(m => m.ElementType).Returns(posts.ElementType);
    dbsetMock.As<IQueryable<Post>>().Setup(m => m.GetEnumerator()).Returns(posts.GetEnumerator());

    var mockContext = new Mock<SachaContextTestDouble>();
    mockContext.Setup(c => c.Posts).Returns(dbsetMock.Object);

    var service = new SomeService(mockContext.Object);
    var retrievedPosts = await service.GetAllAsync();

    var postsList = posts.ToList();

    Assert.AreEqual(posts.Count(), retrievedPosts.Count());
    Assert.AreEqual(postsList[0].Url, retrievedPosts[0].Url);
    Assert.AreEqual(postsList[4].Url, retrievedPosts[4].Url);
}

 

GetAll(),提供 Expression<Func<Post,bool>> 过滤器

我们还可以利用 Expression<Func<Post,bool>> 来对 IQueryable<Post> 应用过滤器。

这是我们试图模拟的代码。

public IEnumerable<Post> GetAll(Expression<Func<Post, bool>> filter)
{
    return context.Posts.Where(filter).AsEnumerable();
}

那么我们如何编写能够实现此功能的测试代码呢?其实并不难,我们只需要巧妙地进行模拟,并确保在进行任何断言之前将过滤器应用于模拟对象,方法如下:

private static Mock<DbSet<T>> CreateMockSet<T>(IQueryable<T> dataForDbSet) where T : class
{
    var dbsetMock = new Mock<DbSet<T>>();

    dbsetMock.As<IQueryable<T>>().Setup(m => m.Provider)
    	.Returns(dataForDbSet.Provider);
    dbsetMock.As<IQueryable<T>>().Setup(m => m.Expression)
    	.Returns(dataForDbSet.Expression);
    dbsetMock.As<IQueryable<T>>().Setup(m => m.ElementType)
    	.Returns(dataForDbSet.ElementType);
    dbsetMock.As<IQueryable<T>>().Setup(m => m.GetEnumerator())
    	.Returns(dataForDbSet.GetEnumerator());
    return dbsetMock;
}


[TestCase]
public void TestGetAllWithLambda()
{
    var posts = Enumerable.Range(0, 5).Select(x => new Post()
    {
        Url = string.Format("www.someurl{0}", x)
    }).ToList();

    for (int i = 0; i < posts.Count; i++)
    {
        posts[i].PostComments.Add(new PostComment()
        {
            Comment = string.Format("some test comment {0}", i)
        });
    }

    var queryablePosts = posts.AsQueryable();

    var dbsetMock = CreateMockSet(queryablePosts);

    var mockContext = new Mock<SachaContextTestDouble>();
    mockContext.Setup(c => c.Posts).Returns(dbsetMock.Object);


    var service = new SomeService(mockContext.Object);

    Func<Post, bool> func = (x) => x.Url == "www.someurl1";
    Expression<Func<Post, bool>> filter = post => func(post);

    var retrievedPosts = service.GetAll(filter);
    CollectionAssert.AreEqual(posts.Where(func).ToList(), retrievedPosts.ToList());
}

FindById()

以下是如何模拟通过直接 Entity Framework 使用发生的 FindById() 调用。可以看到,我们可以简单地返回一个模拟的 Post 对象。

这是我们试图模拟的代码。

public Post FindById(int id)
{
    //NOTE : Even if you included a line like the one below it would include 
    //the PostComments, which seems to be NonLazy
    //this is due to the fact that the Post(s) and Comment(s) are already in the Context
    //var post1 = context.Posts.FirstOrDefault(p => p.Id == id);

    //This should show that we are not doing Lazy Loading and DO NEED to use 
    //Include for navigation properties
    var postWithNoCommentsProof = context.Posts.FirstOrDefault();
    var postWithCommentsThanksToInclude = context.Posts
        .Include(x => x.PostComments).FirstOrDefault();

    var post = context.Posts.Where(p => p.Id == id)
        .Include(x => x.PostComments).FirstOrDefault();
    return post;
}


public async Task<Post> FindByIdAsync(int id)
{
    //NOTE : Even if you included a line like the one below it would include 
    //the PostComments, which seems to be NonLazy
    //this is due to the fact that the Post(s) and Comment(s) are already in the Context
    //var post1 = context.Posts.FirstOrDefault(p => p.Id == id);

    //This should show that we are not doing Lazy Loading and DO NEED to use 
    //Include for navigation properties
    var postWithNoCommentsProof = await context.Posts.FirstOrDefaultAsync();
    var postWithCommentsThanksToInclude = await context.Posts
        .Include(x => x.PostComments).FirstOrDefaultAsync();

    var post = await context.Posts.Where(p => p.Id == id)
        .Include(x => x.PostComments).FirstOrDefaultAsync();
    return post;
}

您可以在下面看到同步和异步版本。

private static Mock<DbSet<T>> CreateMockSet<T>(IQueryable<T> dataForDbSet) where T : class
{
    var dbsetMock = new Mock<DbSet<T>>();

    dbsetMock.As<IQueryable<T>>().Setup(m => m.Provider)
    	.Returns(dataForDbSet.Provider);
    dbsetMock.As<IQueryable<T>>().Setup(m => m.Expression)
    	.Returns(dataForDbSet.Expression);
    dbsetMock.As<IQueryable<T>>().Setup(m => m.ElementType)
    	.Returns(dataForDbSet.ElementType);
    dbsetMock.As<IQueryable<T>>().Setup(m => m.GetEnumerator())
    	.Returns(dataForDbSet.GetEnumerator());
    return dbsetMock;
}

[TestCase]
public void TestFindById()
{
    var posts = Enumerable.Range(0, 5).Select(x => new Post()
    {
        Id = x,
        Url = string.Format("www.someurl{0}", x)
    }).ToList();

    for (int i = 0; i < posts.Count; i++)
    {
        posts[i].PostComments.Add(new PostComment()
        {
            Comment = string.Format("some test comment {0}", i)
        });
    }

    var queryablePosts = posts.AsQueryable();

    var dbsetMock = CreateMockSet(queryablePosts);

    //NOTE : we need to use the string version of Include as the other one that accepts
    //       an Expression tree is an extension method in System.Data.Entity.QueryableExtensions
    //       which Moq doesn't like
    //
    // So the following will not work, as will result in this sort of Exception from Moq
    //
    //       Expression references a method that does not belong to 
    //       the mocked object: m => m.Include<Post,IEnumerable`1>(It.IsAny<Expression`1>())
    //
    // dbsetMock.Setup(m => m.Include(It.IsAny<Expression<Func<Post,IEnumerable<PostComment>>>>()))
    //       .Returns(dbsetMock.Object);
    dbsetMock.Setup(m => m.Include("PostComments")).Returns(dbsetMock.Object);



    var mockContext = new Mock<SachaContextTestDouble>();
    mockContext.Setup(c => c.Posts).Returns(dbsetMock.Object);

    var service = new SomeService(mockContext.Object);
    var retrievedPost = service.FindById(1);

    Assert.AreEqual(retrievedPost.Id,1);
    Assert.IsNotNull(retrievedPost.PostComments);
    Assert.AreEqual(retrievedPost.PostComments.Count,1);
}


[TestCase]
public async Task TestFindByIdAsync()
{
    var posts = Enumerable.Range(0, 5).Select(x => new Post()
    {
        Id = x,
        Url = string.Format("www.someurl{0}", x)
    }).ToList();

    for (int i = 0; i < posts.Count; i++)
    {
        posts[i].PostComments.Add(new PostComment()
        {
            Comment = string.Format("some test comment {0}", i)
        });
    }

    var queryablePosts = posts.AsQueryable();

    var dbsetMock = new Mock<DbSet<Post>>();
    dbsetMock.As<IDbAsyncEnumerable<Post>>()
        .Setup(m => m.GetAsyncEnumerator())
        .Returns(new TestDbAsyncEnumerator<Post>(queryablePosts.GetEnumerator()));

    dbsetMock.As<IQueryable<Post>>()
        .Setup(m => m.Provider)
        .Returns(new TestDbAsyncQueryProvider<Post>(queryablePosts.Provider));

    dbsetMock.As<IQueryable<Post>>().Setup(m => m.Expression).Returns(queryablePosts.Expression);
    dbsetMock.As<IQueryable<Post>>().Setup(m => m.ElementType).Returns(queryablePosts.ElementType);
    dbsetMock.As<IQueryable<Post>>().Setup(m => m.GetEnumerator()).Returns(queryablePosts.GetEnumerator());


    //NOTE : we need to use the string version of Include as the other one that accepts
    //       an Expression tree is an extension method in System.Data.Entity.QueryableExtensions
    //       which Moq doesn't like
    //
    // So the following will not work, as will result in this sort of Exception from Moq
    //
    //       Expression references a method that does not belong to 
    //       the mocked object: m => m.Include<Post,IEnumerable`1>(It.IsAny<Expression`1>())
    //
    // dbsetMock.Setup(m => m.Include(It.IsAny<Expression<Func<Post,IEnumerable<PostComment>>>>()))
    //       .Returns(dbsetMock.Object);
    dbsetMock.Setup(m => m.Include("PostComments")).Returns(dbsetMock.Object);

    var mockContext = new Mock<SachaContextTestDouble>();
    mockContext.Setup(c => c.Posts).Returns(dbsetMock.Object);

    var service = new SomeService(mockContext.Object);
    var retrievedPost = await service.FindByIdAsync(1);

    Assert.AreEqual(retrievedPost.Id, 1);
    Assert.IsNotNull(retrievedPost.PostComments);
    Assert.AreEqual(retrievedPost.PostComments.Count, 1);
}

这是证明这一切正常工作的证据。

 

结论

至此,您可以看到,如今直接使用 Entity Framework 确实是可行的。我希望本文对您有所帮助,并能帮助您测试自己的数据访问层。最终是否使用 Repository 模式的决定不幸不是我的,您需要自己决定,但希望这能为您在选择任何一种方式时提供一些思路。

 

 

© . All rights reserved.