【问题标题】:Decouple EF queries from BL - Extension Methods VS Class-Per-Query将 EF 查询与 BL 解耦 - 扩展方法 VS Class-Per-Query
【发布时间】:2012-06-10 10:22:41
【问题描述】:

我已经阅读了数十篇关于尝试在业务逻辑中模拟\假 EF 的优点和缺点的帖子。 我还没有决定要做什么——但我知道的一件事是——我必须将查询与业务逻辑分开。 在this post 我看到Ladislav 已经回答说有两种好方法:

  1. 让它们在原处,并使用自定义扩展方法、查询视图、映射数据库视图或自定义定义查询来定义可重用部分。
  2. 将每个查询作为方法公开在某个单独的类上。方法 不得公开 IQueryable 且不得接受 Expression 作为参数 = 整个查询逻辑必须包装在方法中。但这会使 你的类涵盖了相关的方法,很像存储库(唯一的一个 可以被嘲笑或伪造)。这个实现接近 与存储过程一起使用的实现。
  1. 您认为哪种方法更好,为什么?
  2. 将查询放在自己的位置是否有任何缺点? (可能会失去一些 EF 或类似的功能)
  3. 我是否必须封装最简单的查询,例如:

    using (MyDbContext entities = new MyDbContext)
    {
        User user = entities.Users.Find(userId);  // ENCAPSULATE THIS ?
    
        // Some BL Code here
    }
    

【问题讨论】:

    标签: entity-framework unit-testing architecture extension-methods


    【解决方案1】:

    所以我猜你的主要观点是你的代码的可测试性,不是吗?在这种情况下,您应该从计算要测试的方法的职责开始,而不是使用单一职责模式重构代码。

    您的示例代码至少有三个职责:

    • 创建对象是一项责任 - 上下文是一个对象。此外,它是您不想在单元测试中使用的对象,因此您必须将其创建移到别处。
    • 执行查询是一项职责。此外,这是您希望在单元测试中避免的责任。
    • 做一些业务逻辑是一种责任

    为了简化测试,您应该重构代码并将这些职责划分为不同的方法。

    public class MyBLClass()
    {
        public void MyBLMethod(int userId)
        {
            using (IMyContext entities = GetContext())
            {
                User user = GetUserFromDb(entities, userId);
    
                // Some BL Code here
            }
        }
    
        protected virtual IMyContext GetContext()
        {
            return new MyDbContext();
        }
    
        protected virtual User GetUserFromDb(IMyDbContext entities, int userId)
        {
            return entities.Users.Find(userId);
        }
    }
    

    现在单元测试业务逻辑应该是小菜一碟,因为您的单元测试可以继承您的类和伪造的上下文工厂方法和查询执行方法,并完全独立于 EF。

    // NUnit unit test
    [TestFixture]
    public class MyBLClassTest : MyBLClass
    {
        private class FakeContext : IMyContext
        {
            // Create just empty implementation of context interface
        }
    
        private User _testUser;
    
        [Test]
        public void MyBLMethod_DoSomething() 
        {
            // Test setup
            int id = 10;
            _testUser = new User 
                { 
                    Id = id, 
                    // rest is your expected test data - that  is what faking is about
                    // faked method returns simply data your test method expects
                };
    
            // Execution of method under test
            MyBLMethod(id);
    
            // Test validation
            // Assert something you expect to happen on _testUser instance 
            // inside MyBLMethod
        }
    
        protected override IMyContext GetContext()
        {
            return new FakeContext();
        }
    
        protected override User GetUserFromDb(IMyContext context, int userId)
        {
            return _testUser.Id == userId ? _testUser : null;
        }
    }
    

    随着您添加更多方法和应用程序的增长,您将重构那些查询执行方法和上下文工厂方法以分离类以遵循对类的单一责任 - 您将获得上下文工厂和一些查询提供程序或在某些情况下存储库(但该存储库将永远不会返回 IQueryable 或在其任何方法中获取 Expression 作为参数)。这也将允许您遵循 DRY 原则,您的上下文创建和最常用的查询将只在一个中心位置定义一次。

    所以最后你可以有这样的东西:

    public class MyBLClass()
    {
        private IContextFactory _contextFactory;
        private IUserQueryProvider _userProvider;
    
        public MyBLClass(IContextFactory contextFactory, IUserQueryProvider userProvider)
        {
            _contextFactory = contextFactory;
            _userProvider = userProvider;
        }
    
        public void MyBLMethod(int userId)
        {
            using (IMyContext entities = _contextFactory.GetContext())
            {
                User user = _userProvider.GetSingle(entities, userId);
    
                // Some BL Code here
            }
        }
    }
    

    这些界面的外观:

    public interface IContextFactory 
    {
        IMyContext GetContext();
    }
    
    public class MyContextFactory : IContextFactory
    {
        public IMyContext GetContext()
        {
            // Here belongs any logic necessary to create context
            // If you for example want to cache context per HTTP request
            // you can implement logic here.
            return new MyDbContext();
        } 
    }
    

    public interface IUserQueryProvider
    {
        User GetUser(int userId);
    
        // Any other reusable queries for user entities
        // Non of queries returns IQueryable or accepts Expression as parameter
        // For example: IEnumerable<User> GetActiveUsers();
    }
    
    public class MyUserQueryProvider : IUserQueryProvider
    {
        public User GetUser(IMyContext context, int userId)
        {
            return context.Users.Find(userId);
        }
    
        // Implementation of other queries
    
        // Only inside query implementations you can use extension methods on IQueryable
    }
    

    您的测试现在将只对上下文工厂和查询提供程序使用假货。

    // NUnit + Moq unit test
    [TestFixture]
    public class MyBLClassTest
    {
        private class FakeContext : IMyContext
        {
            // Create just empty implementation of context interface 
        }
    
        [Test]
        public void MyBLMethod_DoSomething() 
        {
            // Test setup
            int id = 10;
            var user = new User 
                { 
                    Id = id, 
                    // rest is your expected test data - that  is what faking is about
                    // faked method returns simply data your test method expects
                };
    
            var contextFactory = new Mock<IContextFactory>();
            contextFactory.Setup(f => f.GetContext()).Returns(new FakeContext());
    
            var queryProvider = new Mock<IUserQueryProvider>();
            queryProvider.Setup(f => f.GetUser(It.IsAny<IContextFactory>(), id)).Returns(user);
    
            // Execution of method under test
            var myBLClass = new MyBLClass(contextFactory.Object, queryProvider.Object);
            myBLClass.MyBLMethod(id);
    
            // Test validation
            // Assert something you expect to happen on user instance 
            // inside MyBLMethod
        }
    }
    

    如果存储库在将其注入您的业务类之前应该引用传递给其构造函数的上下文,则情况会有所不同。 您的业​​务类仍然可以定义一些从未在任何其他类中使用过的查询——这些查询很可能是其逻辑的一部分。您还可以使用扩展方法来定义查询的某些可重用部分,但您必须始终在要进行单元测试的核心业务逻辑之外使用这些扩展方法(在查询执行方法中或在查询提供程序/存储库中)。这将允许您轻松伪造查询提供程序或查询执行方法。

    我看到 your previous question 并考虑写一篇关于该主题的博客文章,但我对使用 EF 进行测试的意见的核心在于这个答案。

    编辑:

    存储库是与您的原始问题无关的不同主题。特定存储库仍然是有效模式。我们不反对存储库,we are against generic repositories,因为它们不提供任何附加功能并且不解决任何问题。

    问题是存储库本身并不能解决任何问题。必须一起使用三种模式才能形成适当的抽象:存储库、工作单元和规范。这三个都在 EF 中可用:DbSet / ObjectSet 作为存储库,DbContext / ObjectContext 作为工作单元,Linq to Entities 作为规范。到处提到的通用存储库的自定义实现的主要问题是它们仅用自定义实现替换存储库和工作单元,但仍然依赖于原始规范 => 抽象是不完整的,并且在伪造存储库的行为方式相同的测试中泄漏伪造的集合/上下文。

    我的查询提供程序的主要缺点是您需要执行的任何查询的显式方法。在存储库的情况下,您将没有这样的方法,您将只有少数方法接受规范(但这些规范应该在 DRY 原则中定义),它们将构建查询过滤条件、排序等。

    public interface IUserRepository
    {
        User Find(int userId);
        IEnumerable<User> FindAll(ISpecification spec);
    }
    

    这个话题的讨论远远超出了这个问题的范围,它需要你做一些自学。

    顺便说一句。模拟和伪造具有不同的目的 - 如果您需要从依赖项中的方法获取测试数据,则伪造调用;如果需要断言依赖项上的方法是使用预期参数调用的,则模拟调用。

    【讨论】:

    • 我很高兴你是那个回答的人,因为你似乎是关于 EF 可测试性的'Go-to-guy'。我有几个问题:您说 您将获得上下文工厂以及某些查询提供程序或在某些情况下存储库(但该存储库将永远不会返回 IQueryable 或在其任何方法中获取 Expression 作为参数)。这也将允许您遵循 DRY 原则,您的上下文创建和最常用的查询将只在一个中心位置定义一次。 - 我认为您完全反对使用存储库。你能举个例子澄清一下吗?
    • 您能否也澄清一下这句话:'如果存储库在将其注入您的业务类之前应该引用传递给其构造函数的上下文,这会有点不同。 (非常感谢此类存储库的示例以及何时使用它)
    • 最后——你写的最后一个块:你的业务类仍然可以定义一些从未在任何其他类中使用过的查询——这些查询很可能是其逻辑的一部分。 - 你的第一个代码示例使用了这种技术,对吧?我什么时候使用它,什么时候使用扩展方法?这些扩展方法去哪儿了?
    • 请详细说明:在查询执行方法中或在查询提供程序/存储库中。这三种方法有什么区别?查询执行方法只是一种方法(如第一个代码示例中)和查询提供程序仅在一个单独的类中相同吗?以及“存储库”如何在这里发挥作用?
    • 我添加了测试示例以更详细地解释伪造。
    猜你喜欢
    • 2015-03-12
    • 1970-01-01
    • 2011-03-11
    • 2015-11-16
    • 2015-11-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-04
    相关资源
    最近更新 更多