从BL扩展EF查询 - 扩展方法VS Class-Per-Query

时间:2012-06-10 10:22:42

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

我已经阅读过几十篇关于试图在业务逻辑中模拟\假EF的PRO和CON的帖子。 我还没有决定做什么 - 但我知道的一件事是 - 我必须将查询与业务逻辑分开。 在this post我看到拉迪斯拉夫回答说有两种好方法:

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

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

1 个答案:

答案 0 :(得分:7)

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

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

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

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

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到实体作为规范。任何地方提到的通用存储库的自定义实现的主要问题是它们仅使用自定义实现替换存储库和工作单元,但仍依赖于原始规范=&gt;抽象是不完整的,它在测试中泄漏,其中伪造的存储库的行为与伪造的集合/上下文相同。

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

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

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

顺便说一下。模拟和伪造有不同的目的 - 如果你需要从依赖项中的方法获取测试数据,你就假装一个调用,如果需要断言依赖的方法是用预期的参数调用的话,你就模拟调用。