在C#中打破依赖关系的最佳方法?

时间:2011-12-13 10:54:29

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

我们正在考虑将单元测试添加到我们的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);

}

2 个答案:

答案 0 :(得分:7)

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

没有特别推动你进入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容器。如果您的代码是一个很好的因素,它只会测试一个类(并且尽可能经常只测试一个方法),并且您只需要模拟该类的直接依赖关系。

答案 1 :(得分:1)

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

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