【问题标题】:How to test methods where DBContext is created?如何测试创建 DBContext 的方法?
【发布时间】:2015-04-09 21:49:44
【问题描述】:

我没有太多的单元测试经验。例如我在应用程序中有简单的方法:

public void GetName()
    {
        UserRights rights = new UserRights(new DatabaseContext());
        string test = rights.LookupNameByID("12345");
        Console.WriteLine(test);
    }

在这种情况下,我可以通过传递模拟的 DatabaseContext 来测试 UserRights 类的所有方法,但是如何测试 GetName() 方法?最佳做法是什么?应该在哪里创建 DatabaseContext?

【问题讨论】:

  • 什么是UserRights?为什么需要上下文?
  • 理想情况下,您使用依赖注入:使用 Unity 或 Ninject 之类的框架在构造函数中注入上下文和存储库。在您的应用程序中,您可以使用 DI 绑定,而在您的测试中,您只需传递您的模拟依赖项。如果您想了解如何在内存中测试上下文,请查看 my blog。
  • 我觉得自己好蠢……我完全忘记了依赖注入。非常感谢!

标签: c# .net asp.net-mvc entity-framework unit-testing


【解决方案1】:

如果您想以适当隔离的方式(即单元测试)测试 GetName 方法,那么您不能使用 new 在方法本身中创建 UserRights 实例;因为测试实际上只是代码的另一个客户端,因此它不能(也不应该)知道GetName 在内部是如何工作的

所以这意味着对于正确的单元测试,您必须能够将方法的所有依赖项替换为客户端完全控制的依赖项 - 在这种情况下,单元测试中的代码是客户。

在您发布的代码中,客户端代码完全无法控制 UserRights 或 DatabaseContext,因此这是必须更改的第一件事。

您需要重新编写代码,以便客户端可以提供UserRights 实现。事实上,一旦完成,DatabaseContext 来自哪里的问题实际上与单元测试无关,因为它不关心UserRights 本身如何执行其任务!

有很多方法可以做到这一点;你可以使用 Mocks 或 Stubs,你可以使用构造函数或方法注入,你可以使用 UserRights 工厂。这是一个使用非常简单的存根的示例,恕我直言,这是最好的开始方式,并且避免了学习 Mocking 框架——我个人会为此使用 Mocks,但那是因为我很懒 :)

(下面的代码假设包含 GetName 的类称为“UserService”,并使用 xUnit 框架;但 MSTest 也可以正常工作)

假设您可以控制UserService 的代码,因此您可以将LookupNameByID 方法设为虚拟(如果您不能,那么您可能必须使用接口和模拟的路径)

public class UserRights
{
    public virtual LookupNameByID(string id)
    {
        //does whatever a UserRights does.
    }
}

public class UserService
{
    readonly UserRights _rights;
    public UserService(UserRights rights)
    {
       _rights=rights; //null guard omitted for brevity
    }

    public string GetName(string id)
    {
       return _rights.LookupNameByID(id);
    }
}

现在在您的单元测试代码中假设您像这样创建UserRights 的子类:

public class ExplodingUserRights: UserRights
{
     public override string LookupNameByID(string id)
     {
         throw new Exception("BOOM!");
     }
}

现在您可以编写一个测试来查看GetName 在发生不良情况时的反应:

[Fact]
public void LookupNameByID_WhenUserRightsThrowsException_DoesNotReThrow()
{
   //this test will fail if an exception is thrown, thus proving that GetName doesn't handle exceptions correctly.
   var sut = new UserService(new ExplodingUserRights()); <-Look, no DatabaseContext!
   sut.GetName("12345");
}

当好事发生时:

public class HappyUserRights: UserRights
{
     public override string LookupNameByID(string id)
     {
         return "yay!";
     }
}

[Fact]
public void LookupNameByID_ReturnsResultOfUserRightsCall()
{
   //this test will fail if an exception is thrown, thus proving that GetName doesn't handle exceptions correctly.
   var sut = new UserService(new HappyUserRights()); 
   var actual = sut.GetName("12345");
   Assert.Equal("yay!",actual);
}

等等。请注意,我们从来没有接近过DatabaseContext,因为这是一个你只需要在对UserRights 类本身进行单元测试时解决的问题。(那时我可能会建议使用 Jeroen 的建议)从他的链接文章中,并进行集成测试,除非您不能或不会为每个测试设置数据库,在这种情况下您需要使用接口和模拟)

希望对您有所帮助。

【讨论】:

  • 哇!这很让人佩服!非常感谢。
【解决方案2】:

分离代码依赖于 DB 上下文是您需要调查的,这可以使用 Repository pattern 来完成。由于您将 DB Context 传递给对象构造函数,在本例中为 UserRights,因此您可以很容易地更改代码以接收接口(或者简单地将重载的构造函数添加到接受接口的类中,然后在您的单元测试,保留任何现有代码)。有很多方法可以做到这一点,快速谷歌搜索产生了以下文章:

一旦您的类可以接受接口而不是(或作为替代)强类型 DB Context 对象,您就可以使用 Mock 框架来协助测试。查看起订量,了解如何在单元测试中使用这些示例https://github.com/Moq/moq4/wiki/Quickstart

注意:当您的测试需要通过外键关联的数据时,Moq 可能会变得难以处理,语法可能很复杂,难以理解。需要注意的一件事是为了使单元测试更容易设置而对代码进行更改的诱惑,虽然进行此更改可能是有价值的,但它也可能表明您需要重新考虑您的单元测试而不是而不是违反您的应用程序架构和/或设计模式

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多