7

我已经阅读了数十篇关于试图在业务逻辑中模拟\假 EF 的优点和缺点的帖子。我还没有决定要做什么——但我知道的一件事是——我必须将查询与业务逻辑分开。在这篇文章中,我看到 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
    }
    
4

1 回答 1

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

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

我看到了你之前的问题,并考虑写一篇关于该主题的博客文章,但我对使用 EF 进行测试的观点的核心在于这个答案。

编辑:

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

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

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

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

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

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

于 2012-06-10T11:16:22.620 回答