简体   繁体   English

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

[英]Best approach for breaking dependencies in C#?

We are looking at adding unit tests to our C# code base. 我们正在考虑将单元测试添加到我们的C#代码库中。 I am finding it easy to add unit tests to simple classes, but classes that interact with other dependencies are more difficult. 我发现将单元测试添加到简单类很容易,但与其他依赖项交互的类更难。 I have been looking at mocking frameworks, but was wondering about the best approach to write classes in the first place to break external dependencies, like file system, database and messaging system dependencies. 我一直在研究模拟框架,但是想知道最好的方法是首先编写类来打破外部依赖,比如文件系统,数据库和消息系统依赖。

To give an example, a routine listens on a socket for a message in a certain format - say MessageA. 举一个例子,例程在套接字上侦听某种格式的消息 - 比如MessageA。 This is decoded, some calculations are done, this is re-encoded into a different binary format and the resulting message then sent, MessageB. 这被解码,一些计算完成,这被重新编码成不同的二进制格式,然后发送结果消息,MessageB。

My current testing approach is as follows. 我目前的测试方法如下。 I extract an interface for all socket interactions, and create a mock interface. 我为所有套接字交互提取一个接口,并创建一个模拟接口。 I set the interface in a singleton. 我将接口设置为单例。 Then run the class against hard coded inputs. 然后针对硬编码输入运行该类。 The class under test will use the interface in the singleton to send/receive. 被测试的类将使用单例中的接口来发送/接收。

I do a similar thing to test database interactions. 我做了类似的事情来测试数据库交互。

This does not seem like the most flexible approach, how would you go about improving this to make it easier to test? 这似乎不是最灵活的方法,你会如何改进它以使其更容易测试? If a mocking framework is the answer, how would I design the classes? 如果一个模拟框架是答案,我将如何设计类?

Example code : 示例代码:

[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);

}

Instead of using Singletons to set your component implementations, use a Dependency Injection , and a DI library like Ninject . 不使用Singletons来设置组件实现,而是使用依赖注入像Ninject这样DI库 This is exactly the type of scenario they were designed for. 这正是他们设计的场景类型。

Not pushing you to Ninject specifically, but they have a good tutorial :) The concepts will transfer to other frameworks (like Unity ). 没有特别推动你到Ninject,但他们有一个很好的教程:)概念将转移到其他框架(如Unity )。

With DI alone, the code will look something like this: 仅使用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");
  }
}

This looks clean now, but wait until your dependencies have dependencies, or further :) You can use a DI library to solve that, though. 现在看起来很干净,但是等到你的依赖项有依赖关系,或者进一步:)你可以使用DI库来解决这个问题。

With a library to handle the wiring up for you, it will look something like: 有了一个库来处理你的接线,它看起来像:

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();
  }
}

With either of these approaches, plus a mock object framework like Moq , your unit tests look something like this: 使用这些方法中的任何一种,加上像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));
}

You'll notice you can just pass mock instances straight into the code under test, and you don't have to mess around with setting singletons. 你会发现你可以直接将模拟实例传递给测试中的代码,而且你不必乱用设置单例。 This will help you avoid problems like needing to set up the state multiple times, or in between calls. 这将帮助您避免需要多次设置状态或在两次调用之间出现的问题。 It means no hidden dependencies. 这意味着没有隐藏的依赖关系。

You'll also notice I didn't use the DI container in the tests. 您还会注意到我在测试中没有使用DI容器。 If your code is well factored, it will only be testing a single class (and as often as possible, only a single method), and you will only need to mock out the direct dependencies of that class. 如果您的代码是一个很好的因素,它只会测试一个类(并且尽可能经常只测试一个方法),并且您只需要模拟该类的直接依赖关系。

In addition to a DI container (I'm using MS Unity 2.0 currently but there are many to choose from) you will need a good mocking framework, my preference is MOQ. 除了DI容器(我目前使用的是MS Unity 2.0,但有很多可供选择),你需要一个很好的模拟框架,我的偏好是MOQ。 A common pattern/process for breaking concrete dependencies is: 打破具体依赖关系的常见模式/流程是:

  • define the dependency via an interface; 通过接口定义依赖关系; you may luck out and already have an interface, like IDbConnection or you may need to use Proxy to wrap a concrete type and define your own interface. 你可能运气好,并且已经有了一个接口,比如IDbConnection,或者你可能需要使用Proxy来包装一个具体类型并定义你自己的接口。
  • resolve the concrete implementation via your DI container 通过DI容器解决具体实现
  • inject your mock implementations into your DI container at test setup time (inject real impls. at system startup) 在测试设置时将您的模拟实现注入您的DI容器(在系统启动时注入真实的impls。)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM