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

dre*_*iya 5 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);

}
Run Code Online (Sandbox Code Playgroud)

Mer*_*ham 7

不使用Singletons来设置组件实现,而是使用依赖注入像Ninject这样DI库.这正是他们设计的场景类型.

没有特别推动你到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");
  }
}
Run Code Online (Sandbox Code Playgroud)

现在看起来很干净,但是等到你的依赖项有依赖关系,或者进一步:)你可以使用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();
  }
}
Run Code Online (Sandbox Code Playgroud)

使用这些方法中的任何一种,加上像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));
}
Run Code Online (Sandbox Code Playgroud)

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

您还会注意到我在测试中没有使用DI容器.如果您的代码是一个很好的因素,它只会测试一个类(并且尽可能经常只测试一个方法),并且您只需要模拟该类的直接依赖关系.