Лучший подход для разрыва зависимостей в C #? - PullRequest
5 голосов
/ 13 декабря 2011

Мы смотрим на добавление модульных тестов в нашу базу кода 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 ]

7 голосов
/ 13 декабря 2011

Вместо использования Singletons для установки реализаций компонентов используйте Dependency Injection и DI-библиотеку, например 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();
  }
}

При любом из этих подходов, а также фреймворковой модели объектов, такой как 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 голос
/ 13 декабря 2011

В дополнение к контейнеру DI (в настоящее время я использую MS Unity 2.0, но есть из чего выбирать), вам понадобится хорошая среда для моделирования, мое предпочтение - MOQ.Общий шаблон / процесс для разрушения конкретных зависимостей:

  • определяют зависимость через интерфейс;вам может повезти, и у вас уже есть интерфейс, такой как IDbConnection, или вам может понадобиться использовать Proxy, чтобы обернуть конкретный тип и определить свой собственный интерфейс.
  • разрешить конкретную реализацию через контейнер DI
  • внедрить ваши фиктивные реализации в ваш DI-контейнер во время настройки теста (ввести реальные значения при запуске системы)
...