人们如何使用实体框架6进行单元测试,您是否应该感到困扰?

我只是从单元测试和一般的 TDD 开始。我之前已经涉猎过,但现在我决心把它添加到我的工作流程,并编写更好的软件。

我昨天问了一个类似于这样的问题,但它本身似乎就是一个问题。我已经坐下来开始实现一个服务类,我将使用它从控制器中抽象出业务逻辑,并使用 EF6映射到特定的模型和数据交互。

问题是我自己已经设置了路障,因为我不想把 EF 抽象到存储库中(它仍然可以在服务之外用于特定的查询等) ,并且想测试我的服务(将使用 EF Context)。

我想问题是,这么做有意义吗?如果是这样的话,那么在 IQueryable 引起的抽象漏洞以及 Ladislav Mrnka关于单元测试的许多重要文章中,由于 Linq 提供者在处理与特定数据库相对应的内存实现时存在差异,人们是如何在野外做到这一点的呢。

我想测试的代码看起来很简单。(这只是一个虚拟的代码来尝试和理解我在做什么,我想用 TDD 驱动创建)

背景

public interface IContext
{
IDbSet<Product> Products { get; set; }
IDbSet<Category> Categories { get; set; }
int SaveChanges();
}


public class DataContext : DbContext, IContext
{
public IDbSet<Product> Products { get; set; }
public IDbSet<Category> Categories { get; set; }


public DataContext(string connectionString)
: base(connectionString)
{


}
}

服务

public class ProductService : IProductService
{
private IContext _context;


public ProductService(IContext dbContext)
{
_context = dbContext;
}


public IEnumerable<Product> GetAll()
{
var query = from p in _context.Products
select p;


return query;
}
}

目前我的心态是做几件事:

  1. 使用类似这样的方法模仿 EF Context —— 单元测试时嘲笑 EF或者直接在接口上使用模仿框架,比如 moq ——承受单元测试可能通过但不一定能够工作的痛苦,并用集成测试来支持它们?
  2. 也许使用类似 努力的东西来嘲笑 EF-我从来没有使用过它,也不确定是否有其他人在野外使用它?
  3. 不必费心测试任何简单地调用 EF 的东西——所以直接调用 EF 的服务方法(getAll 等)本质上不是单元测试,而只是集成测试?

有没有人在没有回购协议的情况下成功地完成了这件事?

127548 次浏览

我不会对我不拥有的代码进行单元测试。您在这里测试的是什么,MSFT 编译器是否工作?

也就是说,要使这段代码可测试,您几乎必须将数据访问层与业务逻辑代码分离开来。我所做的就是把所有 EF 的内容放到一个(或多个) DAO 或 DAL 类中,这个类也有一个相应的接口。然后我编写我的服务,它将把 DAO 或 DAL 对象作为依赖项(最好是构造函数注入)作为接口引用。现在需要测试的部分(您的代码)可以通过模拟 DAO 接口并将其注入到单元测试中的服务实例中来轻松地进行测试。

//this is testable just inject a mock of IProductDAO during unit testing
public class ProductService : IProductService
{
private IProductDAO _productDAO;


public ProductService(IProductDAO productDAO)
{
_productDAO = productDAO;
}


public List<Product> GetAllProducts()
{
return _productDAO.GetAll();
}


...
}

我认为实时数据访问层是集成测试的一部分,而不是单元测试。我曾经见过一些家伙运行一个验证程序,来验证 Hibernate 到数据库进行了多少次访问,但是他们所在的项目涉及到数据存储中的数十亿条记录,这些额外的访问非常重要。

如果您想测试 单位代码,那么您需要将您想要测试的代码(在本例中是您的服务)与外部资源(例如数据库)隔离开来。您可以使用某种类型的 内存中 EF 提供程序来做到这一点,但是更常见的方法是抽象出 EF 实现,例如使用某种类型的存储库模式。如果没有这种隔离,您编写的任何测试都将是集成测试,而不是单元测试。

至于测试 EF 代码——我为我的存储库编写自动化集成测试,在它们初始化期间向数据库写入各种行,然后调用我的存储库实现,以确保它们按照预期的方式运行(例如,确保结果被正确过滤,或者按照正确的顺序排序)。

这些是集成测试,而不是单元测试,因为测试依赖于存在数据库连接,并且目标数据库已经安装了最新的模式。

这是一个我非常感兴趣的话题。有许多纯粹主义者认为不应该测试 EF 和 NHibernate 这样的技术。他们是正确的,他们已经非常严格的测试,正如以前的答案所说,花费大量的时间测试你没有的东西通常是毫无意义的。

但是,您确实拥有下面的数据库!在我看来,这就是这种方法失败的地方,你不需要测试 EF/NH 是否正确地完成了他们的工作。您需要测试您的映射/实现是否与数据库一起工作。在我看来,这是您可以测试的系统中最重要的部分之一。

然而,严格地说,我们正在从单元测试领域转移到集成测试领域,但是原则仍然是一样的。

您需要做的第一件事情是能够模拟您的 DAL,以便您的 BLL 可以独立于 EF 和 SQL 进行测试。接下来你需要设计你的 集成测试来证明你的 DAL,在我看来这些都是同样重要的。

有几件事情需要考虑:

  1. 对于每个测试,您的数据库需要处于已知状态。大多数系统为此使用备份或创建脚本。
  2. 每个测试必须是可重复的
  3. 每个测试都必须是原子性的

设置数据库有两种主要方法,第一种是运行 UnitTest create DB 脚本。这可以确保您的单元测试数据库在每次测试开始时始终处于相同的状态(您可以重置这个状态,或者在事务中运行每个测试来确保这一点)。

您的另一个选择是我所做的,为每个单独的测试运行特定的设置。我认为这是最好的办法,主要有两个原因:

  • 您的数据库更简单,不需要为每个测试提供完整的模式
  • 每个测试都更安全,如果在创建脚本中更改一个值,它不会使其他几十个测试失效。

不幸的是,你的妥协是速度。运行所有这些测试、运行所有这些安装/拆卸脚本都需要时间。

最后一点,编写如此大量的 SQL 来测试 ORM 可能是非常困难的工作。这就是我采取一种非常讨厌的方法的地方(这里的纯粹主义者将不同意我的观点)。我使用我的 ORM 来创建我的测试!我没有为系统中的每个 DAL 测试使用单独的脚本,而是使用一个测试设置阶段来创建对象,将它们附加到上下文并保存它们。然后我运行我的测试。

这远远不是理想的解决方案,然而在实践中,我发现它更容易管理(特别是当您有几千个测试时) ,否则您将创建大量的脚本。务实胜于纯洁。

毫无疑问,过几年(几个月/几天)我会回顾这个答案,并且不同意自己的观点,因为我的方法已经改变了——但这是我目前的方法。

试着总结一下我上面说过的所有内容,这是我典型的 DB 集成测试:

[Test]
public void LoadUser()
{
this.RunTest(session => // the NH/EF session to attach the objects to
{
var user = new UserAccount("Mr", "Joe", "Bloggs");
session.Save(user);
return user.UserID;
}, id => // the ID of the entity we need to load
{
var user = LoadMyUser(id); // load the entity
Assert.AreEqual("Mr", user.Title); // test your properties
Assert.AreEqual("Joe", user.Firstname);
Assert.AreEqual("Bloggs", user.Lastname);
}
}

这里需要注意的关键是两个循环的会话是完全独立的。在 RunTest 的实现中,必须确保提交和销毁上下文,并且数据只能来自数据库的第二部分。

编辑13/10/2014

我确实说过我可能会在接下来的几个月里修改这个模型。虽然我在很大程度上支持我上面提倡的方法,但是我已经稍微更新了我的测试机制。我现在倾向于在 TestSetup 和 TestTearDown 中创建实体。

[SetUp]
public void Setup()
{
this.SetupTest(session => // the NH/EF session to attach the objects to
{
var user = new UserAccount("Mr", "Joe", "Bloggs");
session.Save(user);
this.UserID =  user.UserID;
});
}


[TearDown]
public void TearDown()
{
this.TearDownDatabase();
}

然后分别测试每个属性

[Test]
public void TestTitle()
{
var user = LoadMyUser(this.UserID); // load the entity
Assert.AreEqual("Mr", user.Title);
}


[Test]
public void TestFirstname()
{
var user = LoadMyUser(this.UserID);
Assert.AreEqual("Joe", user.Firstname);
}


[Test]
public void TestLastname()
{
var user = LoadMyUser(this.UserID);
Assert.AreEqual("Bloggs", user.Lastname);
}

这种做法有几个原因:

  • 没有额外的数据库调用(一次设置,一次拆分)
  • 测试更加细粒度,每个测试验证一个属性
  • 从 Test 方法本身删除 Setup/TearDown 逻辑

我觉得这使得测试类更简单,测试更细粒度(单个断言是好的)

编辑5/3/2015

这种方法的另一个修正。虽然类级别设置对于诸如加载属性之类的测试非常有用,但是在需要不同设置的情况下,它们就没那么有用了。在这种情况下,为每种情况设置一个新类是多余的。

为了帮助解决这个问题,我现在倾向于使用两个基类 SetupPerTestSingleSetup。这两个类根据需要公开框架。

SingleSetup中,我们有一个非常类似的机制,就像我在第一次编辑中描述的那样

public TestProperties : SingleSetup
{
public int UserID {get;set;}


public override DoSetup(ISession session)
{
var user = new User("Joe", "Bloggs");
session.Save(user);
this.UserID = user.UserID;
}


[Test]
public void TestLastname()
{
var user = LoadMyUser(this.UserID); // load the entity
Assert.AreEqual("Bloggs", user.Lastname);
}


[Test]
public void TestFirstname()
{
var user = LoadMyUser(this.UserID);
Assert.AreEqual("Joe", user.Firstname);
}
}

但是,确保只加载正确实体的引用可以使用 SetupPerTest 方法

public TestProperties : SetupPerTest
{
[Test]
public void EnsureCorrectReferenceIsLoaded()
{
int friendID = 0;
this.RunTest(session =>
{
var user = CreateUserWithFriend();
session.Save(user);
friendID = user.Friends.Single().FriendID;
} () =>
{
var user = GetUser();
Assert.AreEqual(friendID, user.Friends.Single().FriendID);
});
}
[Test]
public void EnsureOnlyCorrectFriendsAreLoaded()
{
int userID = 0;
this.RunTest(session =>
{
var user = CreateUserWithFriends(2);
var user2 = CreateUserWithFriends(5);
session.Save(user);
session.Save(user2);
userID = user.UserID;
} () =>
{
var user = GetUser(userID);
Assert.AreEqual(2, user.Friends.Count());
});
}
}

总之,这两种方法的工作取决于您试图测试的内容。

我喜欢将我的过滤器从代码的其他部分分离出来,并在我的博客 http://coding.grax.com/2013/08/testing-custom-linq-filter-operators.html中测试这些过滤器

也就是说,由于 LINQ 表达式和底层查询语言(比如 T-SQL)之间的转换,所测试的过滤器逻辑与运行程序时执行的过滤器逻辑不完全相同。不过,这允许我验证过滤器的逻辑。在测试各层之间的集成之前,我不太担心发生的转换以及大小写敏感性和 null 处理等问题。

努力经验反馈请点击这里

经过大量的阅读,我已经在我的测试中使用了 努力: 在测试期间,上下文是由一个返回内存版本的工厂构建的,该工厂允许我每次对一个空白板进行测试。在测试之外,工厂被解析为一个返回整个 Context 的工厂。

然而,我有一种感觉,对数据库的全功能模拟进行测试往往会拖累测试; 您意识到,为了测试系统的一个部分,您必须注意设置一系列的依赖项。您还倾向于将可能不相关的测试组织在一起,因为只有一个巨大的对象可以处理所有事情。如果您不注意,您可能会发现自己正在做集成测试而不是单元测试

我更喜欢对更抽象的东西进行测试,而不是对一个庞大的 DBContext 进行测试,但是我找不到有意义的测试和基本测试之间的最佳位置。就当是我经验不足吧。

因此,我发现努力很有趣; 如果你需要立即开始工作,它是一个很好的工具,可以迅速开始并取得成果。然而,我认为下一步应该是一些更加优雅和抽象的东西,这也是我接下来要研究的。喜欢这篇文章,看看接下来会发生什么:)

编辑添加 : 热身确实需要一些时间,所以在测试启动时大约需要5秒钟。如果您需要测试套件非常高效,那么这对您来说可能是个问题。


编辑澄清:

我使用“努力”测试了一个 webservice 应用程序。进入的每个消息 M 都通过 Windsor 路由到 IHandlerOf<M>。卡塞尔。Windsor 解析解析组件依赖关系的 IHandlerOf<M>。其中一个依赖项是 DataContextFactory,它允许处理程序请求工厂

在我的测试中,我直接实例化 IHandlerOf 组件,模拟 SUT 的所有子组件,并向处理程序处理工作量包装的 DataContextFactory

这意味着严格意义上我不进行单元测试,因为 DB 受到我的测试的影响。然而,正如我上面所说,它让我立即开始运行,我可以快速测试应用程序中的一些点

我曾经摸索了一段时间,得出以下结论:

如果我的应用程序访问数据库,为什么测试不应该?如果数据访问有问题怎么办?测试必须事先知道它,并提醒自己有关问题。

2-存储库模式有些困难和费时。

所以我想出了这个方法,我不认为这是最好的,但是满足了我的期望:

Use TransactionScope in the tests methods to avoid changes in the database.

这样做是必要的:

1-将 EntityFramework 安装到测试项目中。 2-将连接字符串放入 Test Project 的 app.config 文件中。 3-引用测试项目中的 dll 系统事务。

唯一的副作用是,在尝试插入时,即使事务中止,标识种子也会增加。但是由于测试是针对开发数据库进行的,所以这应该没有问题。

示例代码:

[TestClass]
public class NameValueTest
{
[TestMethod]
public void Edit()
{
NameValueController controller = new NameValueController();


using(var ts = new TransactionScope()) {
Assert.IsNotNull(controller.Edit(new Models.NameValue()
{
NameValueId = 1,
name1 = "1",
name2 = "2",
name3 = "3",
name4 = "4"
}));


//no complete, automatically abort
//ts.Complete();
}
}


[TestMethod]
public void Create()
{
NameValueController controller = new NameValueController();


using (var ts = new TransactionScope())
{
Assert.IsNotNull(controller.Create(new Models.NameValue()
{
name1 = "1",
name2 = "2",
name3 = "3",
name4 = "4"
}));


//no complete, automatically abort
//ts.Complete();
}
}
}

简而言之,我认为不值得为了测试一个只有一行的服务方法来检索模型数据而大费周章。根据我的经验,TDD 的新手想要测试所有的东西。将 facade 抽象为第三方框架,只是为了创建一个模仿框架 API 的老生常谈,这种做法在我看来没什么价值。每个人对于单元测试的好坏都有不同的看法。这些天我倾向于更加务实,问自己我的测试是否真的为最终产品增加了价值,以及代价是什么。

事情是这样的,实体框架是一个实现,尽管它抽象了数据库交互的复杂性,直接交互仍然是紧密耦合的,这就是为什么它让测试感到困惑。

单元测试是独立于任何外部依赖关系(在本例中是数据存储)来测试函数的逻辑及其每个潜在结果。为此,您需要能够控制数据存储的行为。例如,如果您想断言,如果获取的用户不满足某些条件,那么您的函数返回 false,那么您的[模拟]数据存储应该配置为始终返回不满足条件的用户,反之亦然。

既然如此,并且接受 EF 是一个实现的事实,我可能会赞成抽象存储库的想法。似乎有点多余?它不是,因为您正在解决一个将代码与数据实现隔离的问题。

在 DDD 中,存储库只返回聚合根,而不返回 DAO。这样,存储库的使用者就永远不需要知道数据实现(因为它不应该知道) ,我们可以将其作为如何解决这个问题的示例。在这种情况下,EF 生成的对象是 DAO,因此应该对应用程序隐藏。这是您定义的存储库的另一个好处。您可以将业务对象定义为它的返回类型,而不是 EF 对象。现在回购所做的是隐藏对 EF 的调用,并将 EF 响应映射到回购签名中定义的业务对象。现在您可以使用 repo 来代替注入到类中的 DbContext 依赖项,因此,现在您可以模拟该接口来提供您需要的控制,以便单独测试代码。

这有点麻烦,很多人对此嗤之以鼻,但它解决了一个真正的问题。在另一个答案中提到了一个内存提供程序,它可能是一个选项(我还没有尝试过) ,它的存在本身就证明了这种实践的必要性。

我完全不同意最上面的答案,因为它回避了真正的问题,那就是隔离代码,然后切中要害地测试映射。如果您愿意,可以通过各种方式测试您的映射,但是在这里解决实际问题并获得一些真正的代码覆盖率。

我想分享一个方法评论和简要讨论,但显示一个实际的例子,我目前正在使用,以帮助 单元测试基于 EF 的服务。

首先,我希望使用 EF Core 的内存提供程序,但这是关于 EF 6的。此外,对于像 RavenDB 这样的其他存储系统,我也支持通过内存数据库提供商进行测试。同样,这是专门用来帮助测试基于 EF 的代码 没有太多的仪式的。

以下是我设定模式时的目标:

  • 团队中的其他开发人员必须易于理解
  • 它必须将 EF 代码隔离在最低可能的级别
  • 它不能涉及创建奇怪的多责任接口(比如“通用”或“典型”存储库模式)
  • 在单元测试中配置和设置必须容易

我同意以前的说法,EF 仍然是一个实现细节,为了做一个“纯”的单元测试,感觉需要抽象它是可以的。我也同意,理想情况下,我希望确保 EF 代码本身能够工作——但这涉及到沙箱数据库、内存提供程序等等。我的方法解决了这两个问题——您可以安全地单元测试与 EF 相关的代码 还有创建集成测试来专门测试 EF 代码。

我实现这一点的方法是通过简单的 封装 EF 代码转换为专用的 Query 和 Command 类。这个想法很简单: 将任何 EF 代码封装在一个类中,并依赖于最初使用它的类中的一个接口。我需要解决的主要问题是避免向类中添加大量依赖项,以及在测试中设置大量代码。

这就需要一个有用的、简单的库: 调解员。它支持简单的进程内消息传递,并且通过将“请求”与实现代码的处理程序分离来实现。这还有一个额外的好处,那就是将“什么”与“如何”脱钩。例如,通过将 EF 代码封装成小块,它允许您用另一个提供者或完全不同的机制替换实现,因为您所做的一切就是发送一个请求来执行一个操作。

利用依赖注入(有或没有一个框架——你的偏好) ,我们可以很容易地模拟中介和控制请求/响应机制,以启用单元测试 EF 代码。

首先,假设我们有一个需要测试的具有业务逻辑的服务:

public class FeatureService {


private readonly IMediator _mediator;


public FeatureService(IMediator mediator) {
_mediator = mediator;
}


public async Task ComplexBusinessLogic() {
// retrieve relevant objects


var results = await _mediator.Send(new GetRelevantDbObjectsQuery());
// normally, this would have looked like...
// var results = _myDbContext.DbObjects.Where(x => foo).ToList();


// perform business logic
// ...
}
}

你开始看到这种方法的好处了吗?明确地不仅将所有与 EF 相关的代码封装到描述性类中,而且还通过消除“如何”处理这个请求的实现问题来实现可扩展性——这个类不关心相关对象是来自 EF、 MongoDB 还是文本文件。

现在通过 MediatR 处理请求和处理程序:

public class GetRelevantDbObjectsQuery : IRequest<DbObject[]> {
// no input needed for this particular request,
// but you would simply add plain properties here if needed
}


public class GetRelevantDbObjectsEFQueryHandler : IRequestHandler<GetRelevantDbObjectsQuery, DbObject[]> {
private readonly IDbContext _db;


public GetRelevantDbObjectsEFQueryHandler(IDbContext db) {
_db = db;
}


public DbObject[] Handle(GetRelevantDbObjectsQuery message) {
return _db.DbObjects.Where(foo => bar).ToList();
}
}

正如您所看到的,抽象是简单和封装的。它也是 完全可以检测,因为在集成测试中,可以单独测试这个类——这里没有混合的业务关注点。

那么,我们的特性服务的单元测试是什么样子的呢?很简单。在这个例子中,我使用 莫克来进行嘲讽(使用任何让你高兴的东西) :

[TestClass]
public class FeatureServiceTests {


// mock of Mediator to handle request/responses
private Mock<IMediator> _mediator;


// subject under test
private FeatureService _sut;


[TestInitialize]
public void Setup() {


// set up Mediator mock
_mediator = new Mock<IMediator>(MockBehavior.Strict);


// inject mock as dependency
_sut = new FeatureService(_mediator.Object);
}


[TestCleanup]
public void Teardown() {


// ensure we have called or expected all calls to Mediator
_mediator.VerifyAll();
}


[TestMethod]
public void ComplexBusinessLogic_Does_What_I_Expect() {
var dbObjects = new List<DbObject>() {
// set up any test objects
new DbObject() { }
};


// arrange


// setup Mediator to return our fake objects when it receives a message to perform our query
// in practice, I find it better to create an extension method that encapsulates this setup here
_mediator.Setup(x => x.Send(It.IsAny<GetRelevantDbObjectsQuery>(), default(CancellationToken)).ReturnsAsync(dbObjects.ToArray()).Callback(
(GetRelevantDbObjectsQuery message, CancellationToken token) => {
// using Moq Callback functionality, you can make assertions
// on expected request being passed in
Assert.IsNotNull(message);
});


// act
_sut.ComplexBusinessLogic();


// assertions
}


}

您可以看到,我们所需要的只是一个设置,我们甚至不需要配置任何额外的东西——这是一个非常简单的单元测试。我们把话说清楚:这是完全可以做的 没有类似中介(你可以简单地实现一个接口,并模拟它的测试,例如 IGetRelevantDbObjectsQuery) ,但在实践中,对于一个大型代码库的许多功能和查询/命令,我喜欢封装和内在 DI 支持中介提供。

如果你想知道我是如何组织这些课程的,其实很简单:

- MyProject
- Features
- MyFeature
- Queries
- Commands
- Services
- DependencyConfig.cs (Ninject feature modules)

按照特性片 进行组织并不重要,但是这样可以将所有相关/依赖的代码保持在一起,并且很容易发现。最重要的是,我按照 命令/查询分离原则分离查询和命令。

这符合我的所有标准: 它不拘礼节,容易理解,而且还有额外的隐藏好处。例如,如何处理保存更改?现在,您可以通过使用角色接口(IUnitOfWork.SaveChangesAsync())和对单个角色接口的模拟调用来简化 Db Context,或者您可以在 RequestHandlers 中封装提交/回滚——不管您喜欢怎么做,只要它是可维护的,这都取决于您。例如,我曾试图创建一个通用的请求/处理程序,你只需要传递一个 EF 对象,它就会保存/更新/删除它——但是你必须询问你的意图是什么,并且记住,如果你想用另一个存储提供者/实现来替换处理程序,你可能应该创建显式的命令/查询来表示你想要做什么。通常情况下,单个服务或特性需要某些特定的东西——在您有需要之前不要创建通用的东西。

这种模式有 当然的注意事项——使用简单的发布/订阅机制可能会走得太远。我已经将我的实现限制在只抽象 EF 相关的代码,但是冒险的开发人员可以开始使用 MediatR 来过度和消息化一切——一些好的代码审查实践和同行审查应该抓住的东西。这是一个过程问题,而不是中介 R 的问题,所以只要认识到如何使用这种模式。

您想要一个关于人们如何进行单元测试/模拟 EF 的具体例子,这是一种在我们的项目中成功工作的方法——团队对于采用这种方法的容易程度感到非常高兴。希望这个能帮上忙!与编程中的所有事情一样,有多种方法,这完全取决于您想要实现的目标。我重视简单性、易用性、可维护性和可发现性——这个解决方案满足了所有这些要求。

有一个内存实体框架数据库提供程序,它叫“努力”。我还没有真正尝试过... 哈哈刚刚发现这是提到了问题!

或者,您可以切换到 EntityFrameworkCore,它具有内置的内存数据库提供程序。

Https://blog.goyello.com/2016/07/14/save-time-mocking-use-your-real-entity-framework-dbcontext-in-unit-tests/

Https://github.com/tamasflamich/effort

我使用了一个工厂来获取上下文,这样我就可以创建接近其使用的上下文。这似乎工作在本地的视觉工作室,但不在我的 TeamCity 构建服务器上,还不确定为什么。

return new MyContext(@"Server=(localdb)\mssqllocaldb;Database=EFProviders.InMemory;Trusted_Connection=True;");

测试您期望实体框架做什么(即验证您的期望)很重要。我已经成功地使用了一种方法,就是使用 moq,如下例所示(复制到这个答案的长度) :

Https://learn.microsoft.com/en-us/ef/ef6/fundamentals/testing/mocking

但是要小心... ... SQL 上下文不能保证以特定的顺序返回内容,除非您在 linq 查询中有一个合适的“ OrderBy”,所以当您使用内存中列表(linq-to-tity)进行测试时,可以编写通过的内容,但是当您使用 uat/live 环境(linq-to-SQL)时,可以编写失败的内容。

为了使 单位测试代码依赖于您的数据库,您需要为每个测试设置一个数据库或者模拟。

  1. 对于所有测试,如果数据库(真实的或模拟的)具有单个状态,那么您将很快受到影响; 您无法测试所有记录都是有效的,而且有些记录来自不同的数据。
  2. 在 OneTimeSetup 中设置一个内存数据库会出现问题,在下一个测试开始之前,旧数据库没有被清除。当您单独运行它们时,这将显示为正在工作的测试,但是当您运行所有测试时,它们将失败。
  3. 单元测试理想情况下应该只设置影响测试的内容

我在一个应用程序中工作,这个应用程序有很多表,有很多连接和一些大量的 Linq 块。这些 需要测试。错过一个简单的分组,或者导致多于1行的联接将影响结果。

为了解决这个问题,我设置了一个重量级的单元测试助手(Unit Test Helper) ,这需要大量的工作来设置,但是它使我们能够在任何状态下可靠地模拟数据库,并且针对55个相互连接的表运行48个测试,整个数据库设置48次需要4.7秒。

方法如下:

  1. 在 Db 上下文类中,确保将每个表类设置为 Virtual

    public virtual DbSet<Branch> Branches { get; set; }
    public virtual DbSet<Warehouse> Warehouses { get; set; }
    
  2. 在 UnitTestHelper 类中创建一个设置数据库的方法。每个表类都是一个可选参数。如果没有提供,它将通过 Make 方法创建

    internal static Db Bootstrap(bool onlyMockPassedTables = false, List<Branch> branches = null, List<Products> products = null, List<Warehouses> warehouses = null)
    {
    if (onlyMockPassedTables == false) {
    branches ??= new List<Branch> { MakeBranch() };
    warehouses ??= new List<Warehouse>{ MakeWarehouse() };
    }
    
  3. 对于每个表类,其中的每个对象都映射到其他列表

        branches?.ForEach(b => {
    b.Warehouse = warehouses.FirstOrDefault(w => w.ID == b.WarehouseID);
    });
    
    
    warehouses?.ForEach(w => {
    w.Branches = branches.Where(b => b.WarehouseID == w.ID);
    });
    
  4. 并将其添加到 DbContext

         var context = new Db(new DbContextOptionsBuilder<Db>().UseInMemoryDatabase(Guid.NewGuid().ToString()).Options);
    context.Branches.AddRange(branches);
    context.Warehouses.AddRange(warehouses);
    context.SaveChanges();
    return context;
    }
    
  5. 定义要创建的 ID 列表更容易重用它们,并确保联接有效

     internal const int BranchID = 1;
    internal const int WarehouseID = 2;
    
  6. 为每个表创建一个 Make,以设置最基本但可以连接的版本

     internal static Branch MakeBranch(int id = BranchID, string code = "The branch", int warehouseId = WarehouseID) => new Branch { ID = id, Code = code, WarehouseID = warehouseId };
    internal static Warehouse MakeWarehouse(int id = WarehouseID, string code = "B", string name = "My Big Warehouse") => new Warehouse { ID = id, Code = code, Name = name };
    

这是一项繁重的工作,但它只需要执行一次,然后您的测试就可以非常集中,因为数据库的其余部分将为它进行设置。

[Test]
[TestCase(new string [] {"ABC", "DEF"}, "ABC", ExpectedResult = 1)]
[TestCase(new string [] {"ABC", "BCD"}, "BC", ExpectedResult = 2)]
[TestCase(new string [] {"ABC"}, "EF", ExpectedResult = 0)]
[TestCase(new string[] { "ABC", "DEF" }, "abc", ExpectedResult = 1)]
public int Given_SearchingForBranchByName_Then_ReturnCount(string[] codesInDatabase, string searchString)
{
// Arrange
var branches = codesInDatabase.Select(x => UnitTestHelpers.MakeBranch(code: $"qqqq{x}qqq")).ToList();
var db = UnitTestHelpers.Bootstrap(branches: branches);
var service = new BranchService(db);


// Act
var result = service.SearchByName(searchString);


// Assert
return result.Count();
}