我已经阅读了数十篇关于在业务逻辑中尝试模拟\假EF的优点和缺点的文章。
我还没有决定要做什么-但我知道的一件事-我必须将查询与业务逻辑分开。
在this post中,我看到拉迪斯拉夫回答了2种好方法:
using (MyDbContext entities = new MyDbContext)
{
User user = entities.Users.Find(userId); // ENCAPSULATE THIS ?
// Some BL Code here
}
最佳答案
所以我想您的重点是代码的可测试性,不是吗?在这种情况下,您应该首先计算要测试的方法的职责,然后使用单一职责模式重构代码。
您的示例代码至少具有以下三个职责:
为了简化测试,您应该重构代码并将这些职责划分为单独的方法。
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
}
}
}
这些接口(interface)的外观如下:
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
}
}
对于存储库,在将其注入(inject)到您的业务类之前,应先引用传递给其构造函数的上下文,这会有所不同。
您的业务类仍然可以定义一些从未在其他任何类中使用的查询-这些查询很可能是其逻辑的一部分。您还可以使用扩展方法来定义查询的某些可重用部分,但是必须始终在要进行单元测试的核心业务逻辑之外使用这些扩展方法(在查询执行方法中或在查询提供者/存储库中)。这将使您轻松伪造查询提供程序或查询执行方法。
我看到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);
}
关于这个主题的讨论远远超出了这个问题的范围,它需要您进行一些自学。
顺便提一句。模拟和伪造的目的不同-如果需要从依赖项中的方法获取测试数据,则可以伪造一个调用;如果需要断言依赖项中的方法是用预期参数调用的,则可以模拟该调用。