【问题标题】:Best approach for breaking dependencies in C#?在 C# 中打破依赖关系的最佳方法?
【发布时间】:2011-12-13 10:54:29
【问题描述】:

我们正在考虑将单元测试添加到我们的 C# 代码库中。我发现将单元测试添加到简单的类很容易,但与其他依赖项交互的类更加困难。我一直在研究模拟框架,但想知道首先编写类以打破外部依赖关系的最佳方法,例如文件系统、数据库和消息系统依赖关系。

举个例子,例程在套接字上侦听某种格式的消息——比如 MessageA。这被解码,一些计算完成,被重新编码成不同的二进制格式,然后发送结果消息,MessageB。

我目前的测试方法如下。我为所有套接字交互提取了一个接口,并创建了一个模拟接口。我将接口设置为单例。然后针对硬编码输入运行该类。被测类将使用单例中的接口发送/接收。

我做类似的事情来测试数据库交互。

这似乎不是最灵活的方法,您将如何改进它以使其更易于测试?如果一个模拟框架是答案,我将如何设计这些类?

示例代码:

[SetUp]
public void init()
{
    // set message interface in singleton as mock interface
    CommAdapter.Instance.MessageAdapter = new MockMessage();

    // build reference message from hard coded test variables
    initialiseMessageA();

    // set input from mock message socket
    ((MockMessage) CommAdapter.Instance.MessageAdapter).MessageIn = m_messageA;
}

[Test]
public void test_listenMessage_validOutput()
{
    // initialise test class
    MessageSocket tS = new MessageSocket();

    // read from socket
    tS.listenMessage();

    // extract mock interface from singleton
    MockMessage mm = ((MockMessage) CommAdapter.Instance.MessageAdapter);

    // assert sent message is in correct / correstpoinding format
    Assert.AreEqual(1000001, mm.SentMessageB.TestField);

}

【问题讨论】:

  • Drexiya,如果你有时间,我强烈建议你阅读Growing Object-Oriented Software, Guided by Tests。这是一本 300 页,易于阅读的书(您可以在一个周末完成)。这是一本很好的书,描述了如何正确地进行单元测试,并讨论了很多关于如何使用模拟的内容。书中的代码示例是用 Java 编写的,但我相信您可以将抽象的想法应用到 C# 中,使用 moq 或类似的模拟库。
  • 感谢奥古斯托的推荐,我一定会去看看那本书。

标签: c# unit-testing dependency-injection mocking


【解决方案1】:

不要使用单例来设置组件实现,而是使用Dependency InjectionDI library like Ninject。这正是他们设计的场景类型。

没有专门将您推向 Ninject,但他们有一个很好的教程 :) 这些概念将转移到其他框架(例如 Unity)。

单独使用 DI,代码将如下所示:

class Samurai {
  private IWeapon _weapon;
  public Samurai(IWeapon weapon) {
    _weapon = weapon;
  }
  public void Attack(string target) {
    _weapon.Hit(target);
  }
}

class Shuriken : IWeapon {
  public void Hit(string target) {
    Console.WriteLine("Pierced {0}'s armor", target);
  }
}

class Program {
  public static void Main() {
    Samurai warrior1 = new Samurai(new Shuriken());
    Samurai warrior2 = new Samurai(new Sword());
    warrior1.Attack("the evildoers");
    warrior2.Attack("the evildoers");
  }
}

现在看起来很干净,但请等到您的依赖项具有依赖项,或者更进一步 :) 不过,您可以使用 DI 库来解决这个问题。

使用一个库来为您处理接线,它看起来像:

class Program {
  public static void Main() {
    using(IKernel kernel = new StandardKernel(new WeaponsModule()))
    {
      var samurai = kernel.Get<Samurai>();
      warrior1.Attack("the evildoers");
    }
  }
}

// Todo: Duplicate class definitions from above...

public class WarriorModule : NinjectModule {
  public override void Load() {
    Bind<IWeapon>().To<Sword>();
    Bind<Samurai>().ToSelf().InSingletonScope();
  }
}

使用这两种方法中的任何一种,加上a mock object framework like Moq,您的单元测试看起来像这样:

[Test]
public void HitShouldBeCalledByAttack()
{
    // Arrange all our data for testing
    const string target = "the evildoers";
    var mock = new Mock<IWeapon>();
    mock.Setup(w => w.Hit(target))
        .AtMostOnce();

    IWeapon mockWeapon = mock.Object;
    var warrior1 = new Samurai(mockWeapon);

    // Act on our code under test
    warrior1.Attack(target);

    // Assert Hit was called
    mock.Verify(w => w.Hit(target));
}

您会注意到,您可以直接将模拟实例传递到被测代码中,而不必乱设置单例。这将帮助您避免需要多次设置状态或在两次调用之间设置状态等问题。这意味着没有隐藏的依赖关系。

您还会注意到我没有在测试中使用 DI 容器。如果您的代码被很好地分解,它将只测试一个类(并且尽可能多地只测试一个方法),并且您只需要模拟出该类的直接依赖关系。

【讨论】:

  • 嗨梅林,感谢您的详细回复。我将研究这种类型的解决方案。在我沿着这条路线走得太远之前,您是否认为这对于只需要有限灵活性的应用程序来说太过分了?
  • 另外一个问题是我将添加到代码中的开销。我看到一些基准表明 Ninject 可能有点慢。
  • 一个网站声称 StructureMap 是一种快速的解决方案,但他们的介绍指出:“如果应用程序或流程对灵活性要求不高,请不要使用 StructureMap。 StructureMap 提供的抽象和间接性在更简单的系统或流程中是不必要的,甚至是有害的。'
  • @drexiya:您在应用程序根目录中使用过一次 DI 容器。所以你应该只在启动时看到减速。我没有注意到它很慢。无论如何,有大量的 DI 库 :) 至于“小灵活性”,您的应用程序需要小灵活性是什么意思?能够用模拟对象替换依赖项对我来说听起来像是灵活性要求......
  • @drexiya:很像第一个示例,除了 new TopLevel(new FirstDep(new SecondDep(), new SecondDep2())) 等。然后你不需要 DI 库,只需要 DI 模式。这也是 DI 所能达到的最快速度。
【解决方案2】:

除了 DI 容器(我目前使用的是 MS Unity 2.0,但有很多可供选择)之外,您还需要一个好的模拟框架,我的偏好是 MOQ。打破具体依赖关系的常见模式/过程是:

  • 通过接口定义依赖;你可能很幸运并且已经有了一个接口,比如 IDbConnection,或者你可能需要使用 Proxy 来包装一个具体的类型并定义你自己的接口。
  • 通过您的 DI 容器解决具体实现
  • 在测试设置时将您的模拟实现注入您的 DI 容器(在系统启动时注入真实的实现)

【讨论】:

  • 道歉@Merlyn Morgan-Graham,这几乎是相同的答案
  • 无需道歉。虽然 SO 并没有特别宽恕重复的答案,但它也没有真正阻止他们。假设它们将是不可避免的——人们经常同时阅读这个问题。投票解决了这个问题。 +1 在这个答案上,因为它仍然很好,而且很紧凑。我建议您在单元测试中跳过使用 DI container。我认为没有必要。
最近更新 更多