6

我一直试图找到一个好的解决方案,但没有运气,所以要么我没有搜索正确的关键字,要么我们从一开始就做错了,所以问题不应该真的存在。

更新澄清:我希望它作为一个单元测试而不是作为一个集成测试工作,所以我不希望它进入数据库,但我想模拟当 EF 在我的单元测试中持久化更改时所做的关联。

原始问题:

假设您正在测试这样的服务方法:

[Test]
public void AssignAuthorToBook_NewBookNewAuthor_SuccessfullyAssigned()
{
  IBookService service = new BookService();

  var book = new Book();

  var Author = new Author() { Id = 123 };

  service.AssignAuthorToBook(book, author);

  Assert.AreEqual(book.AuthorId, 123);
}

现在让我们说这个测试失败了,因为它AssignAuthorToBook实际上是使用代码工作的,book.Author = author;所以它没有分配AuthorId,它正在分配实体。当SaveChanges()在上下文中使用实体框架方法将其持久化时,它将关联实体并且 ID 将关联。但是,在我上面的示例中,不会应用实体框架的逻辑。我要说的是代码一旦SaveChanges()被调用就可以工作,但是单元测试会失败。

在这个简单的示例中,您可能会立即知道测试失败的原因,因为您刚刚在代码之前编写了测试并且可以轻松地修复它。但是,对于更复杂的操作以及未来可能会改变实体关联方式的操作,这将破坏测试但可能不会破坏功能,如何最好地进行单元测试?

我的想法是:

  • 服务层应该不知道持久层——我们应该在单元测试中模拟数据上下文来模拟它的工作方式吗?是否有一种简单的方法可以自动绑定关联(即如果使用则分配给正确的实体,Id或者如果使用实体则分配正确Id的实体)?
  • 还是应该以稍微不同的方式构建测试?

我继承的当前项目中存在的测试与我上面的示例一样工作,但它让我觉得方法有问题,而且我还没有设法找到解决可能常见问题的简单解决方案。我相信应该模拟数据上下文,但这似乎需要将大量代码添加到模拟中以动态创建关联 - 这肯定已经解决了吗?

更新:这些是迄今为止我找到的最接近的答案,但它们并不是我所追求的。我不想这样测试 EF,我只是想知道测试访问存储库的服务方法的最佳实践是什么(直接或通过共享相同上下文的其他存储库的导航属性)。

如何模拟实体框架的导航属性智能?

模拟数据上下文和外键/导航属性

Entity Framework 4.1 的假 DbContext 进行测试

使用 ADO.NET 模拟上下文生成器时未设置导航属性

到目前为止的结论:使用单元测试是不可能的,只有使用真实数据库的集成测试才有可能。您可以接近并可能编写一些代码来动态关联导航属性,但是您的模拟数据上下文永远不会完全复制真实的上下文。如果有任何解决方案使我能够自动关联导航属性,这将使我的单元测试更好,我会很高兴,如果不是完美的话(成功的单元测试的性质无论如何都不能保证功能)ADO.NET模拟上下文生成器很接近,但似乎我必须拥有每个实体的模拟版本,如果在我的实现中使用部分类将功能添加到它们中,这对我不起作用。

4

7 回答 7

5

我认为您期望您的测试结果暗示使用多个依赖项,可以说不将其限定为单元测试,特别是因为隐含对 EF 的依赖。

这里的想法是,如果你承认你的 BookService 依赖于 EF,你应该使用一个模拟来断言它与它正确交互,不幸的是 EF 似乎不喜欢被模拟,所以我们总是可以把它放在一个存储库下,这是一个如何使用 Moq 编写该测试的示例:

[Test]
public void AssignAuthorToBook_NewBookNewAuthor_CreatesNewBookAndAuthorAndAssociatesThem()
{
  var bookRepositoryMock = new Mock<IBookRepository>(MockBehavior.Loose);
  IBookService service = new BookService(bookRepositoryMock.Object);
  var book = new Book() {Id = 0}
  var author = new Author() {Id = 0}; 

  service.AssignAuthorToBook(book, author);

  bookRepositoryMock.Verify(repo => repo.AddNewBook(book));
  bookRepositoryMock.Verify(repo => repo.AddNewAuthor(author));
  bookRepositoryMock.Verfify(repo => repo.AssignAuthorToBook(book, author));
}

正在设置的 id 是您可以使用集成测试的东西,但我认为您不应该担心 EF 无法设置 Id,我这样说的原因与您不应该担心测试是否.net 框架做了它应该做的事情。

我过去写过有关交互测试的文章(我认为在这种情况下这是正确的方法,您正在测试 BookService 和 Repository 之间的交互),希望对您有所帮助:http ://blinkingcaret.wordpress.com /2012/11/20/interaction-testing-fakes-mocks-and-stubs/

于 2013-08-21T09:01:28.280 回答
2

我遇到了和你一样的问题,看到了你的帖子。之后我发现了一个名为 Effort 的内存数据库。看看努力

以下测试正常工作

        EntityConnection conn = Effort.EntityConnectionFactory.CreateTransient("name=MyEntities");

        MyEntities ctx = new MyEntities(conn);

        JobStatus js = new JobStatus();
        js.JobStatusId = 1;
        js.Description= "New";

        ctx.JobStatuses.Add(js);

        Job j = new Job();
        j.JobId = 1;
        j.JobStatus = js;
        ctx.Jobs.Add(j);

        ctx.SaveChanges();

        Assert.AreEqual(j.JobStatusId, 1);

其中 MyEntities 是使用 Effort 连接字符串创建的 DbContext。

您仍然需要在内存中创建对象,但是,在上下文中保存更改会像数据库一样设置对象关联。

于 2013-08-26T19:30:32.393 回答
1

一般的解决方案是把它分成几层。

  • 持久层/存储库模式负责从您选择的任何存储中写出/读取信息。ORM 应该装在持久层内。在这一层之上,应该没有 ORM 的痕迹。该层返回 DDD 书中定义的实体/值对象(我不打算与 EF 相关的任何内容)
  • 接下来,服务层调用 Repository 接口来获取这些 POCO 实体/值对象,并负责领域/业务逻辑。

至于测试,

  • 持久层的基本目的是持久化所需的信息。例如,将客户信息写入文件或数据库。因此,使用特定于实现的技术(例如 SQL)对这一层进行集成测试是最有意义的。如果测试通过但该层无法读取/写入实际的 SQL DB.. 它们是无用的。
  • 服务层测试可以模拟持久层入口接口(例如 Repository 接口)并验证域逻辑,而不依赖于持久性技术——这是应该的方式。单元测试在这里。
于 2013-08-23T06:44:02.753 回答
1

你的问题是:如果你模拟数据库操作,你无法测试AssignAuthorToBook的正确功能,因为这个类与Entity Framework高度耦合,行为会改变。

所以我的解决方案:解耦类(使用DAO,一个包含所有数据库操作的接口),现在AssignAuthorToBook很容易测试,因为使用了SetBook / GetBookId的功能。并为您的操作(SetBook / GetBookId)测试数据库(好吧,这不是一个明确的单元测试,但您的问题正是:我可以测试谁来测试数据库操作?,所以......测试它,但是在单独测试。

于 2013-08-20T18:46:13.660 回答
1

这永远不会像你拥有的那样工作。数据上下文在你的服务层的幕后被包裹起来,BookID 的关联永远不会在你的本地变量上得到更新。

当我使用 EF 对某事进行 TDD 时,我通常将所有 EF 逻辑包装到某种 DAO 中,并为实体提供 CRUD 方法。

然后你可以做这样的事情:

[Test]
public void AssignAuthorToBook_NewBookNewAuthor_SuccessfullyAssigned()
{
  IBookService service = new BookService();

  var book = new Book();
  var bookID = 122;
  book.ID = bookId;
  var Author = new Author() { Id = 123 };

  service.AssignAuthorToBook(book, author);

//ask the service for the book, which uses EF to get the book and populate the navigational properties, etc...
  book = service.GetBook(bookID)

  Assert.AreEqual(book.AuthorId, 123);
}
于 2013-08-14T15:51:42.717 回答
0

如果您想测试“AssignAuthorToBook”是否按预期工作,那么您可以完全不了解数据库 - 您模拟了 Book 对象并验证使用正确的值调用了正确的设置器。所有与持久性相关的东西都应该被存根。要验证是否调用了 setter 或方法,您可以使用MoqRhinoMocks

可以在这里找到一个示例:Rhino Mocks: AAA Synax: Assert property was set with a given type

以下是如何验证替代期望的示例:使用 RhinoMocks,我如何断言调用了几种方法之一?

于 2013-08-15T09:57:13.653 回答
0

我可能错了,但据我所知,即使没有持久层,您的域模型也应该是一致的。

因此,在这种情况下,您的服务应确保属性 AuthorID 等于 Assigned Author 类,甚至在将其持久化到数据库之前。或者您的 Book AuthorID 属性获取器从服务分配的其内部 Author 类 ID 属性中获取该信息。

public class Book {
    public Author Author { get; set; }
    public int AuthorId { 
        get { return Author.ID; }
        set { Author.ID = value; }
    }
}

public class Author {
    public int Id { get; set; }
}

public class BookService {
    public void AssignAuthorToBook(Book book, Author author)
    {
        book.Author = author;
    }
}
于 2013-08-23T12:56:44.183 回答