我看过几十约试图嘲笑\假EF业务逻辑利弊职位。 我还没有决定做什么 - 但有一点我知道的是 - 我要查询从业务逻辑分离。 在这篇文章我看到拉吉斯拉夫已回答有2个好办法:
- 让他们有他们和使用自定义扩展方法,查询视图,映射数据库视图或自定义的定义查询定义可重复使用的部分在哪里。
- 暴露每一个查询作为一些单独的类方法。 该方法必须不暴露IQueryable的,并且必须不接受表达作为参数=整个查询逻辑必须被包裹在所述的方法。 但是,这将使你的类覆盖相关的方法很像库(可嘲笑或伪造的唯一的一个)。 这种实现是接近与存储过程中使用的实现。
- 你认为哪种方法更好任何为什么呢?
- 有没有把查询自己发生的任何不足之处? (也许失去从EF或一些功能类似的东西)
我必须封装甚至像最简单的查询:
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
}
}
}
如果这些接口的样子:
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 /对象集作为仓库,的DbContext / ObjectContext的作为工作的单位和LINQ to Entities作为规范。 到处都提到通用库的自定义实现的主要问题是,他们只更换自定义实施工作的仓库和单位,但仍取决于原规格=>抽象是不完整的,在那里伪造库的方式相同的行为是在测试中泄露伪造设置/上下文。
我的查询提供的主要缺点是,你将需要执行的任何查询显式方法。 在仓库的情况下,你不会有这样的方法,你将只有几个方法接受规范(但同样这些规范应当在DRY原则定义)将建立查询过滤条件,订货等。
public interface IUserRepository
{
User Find(int userId);
IEnumerable<User> FindAll(ISpecification spec);
}
这个话题的讨论远远超出了这个问题的范围,它需要你做一些自学。
顺便说一句。 嘲讽和伪造有不同的目的 - 你假一通话,如果你需要从依赖方法得到的测试数据,你嘲笑电话,如果您需要断言的依赖这种方法被称为预计参数。