Как я могу корректно использовать методы модульного тестирования, которые зависят друг от друга?

#c# #unit-testing

#c# #модульное тестирование

Вопрос:

Рассмотрим этот код:

     private readonly Dictionary<Type, Component> _components = new Dictionary<Type, Component>();

    public Component this [Type type]
    {
        get
        {
            Component component;
            _components.TryGetValue(type, out component);
            return component;
        }
    }

    public void AddComponent(Component component)
    {
        _components.Add(component.GetType(), component);
    }
  

Как вы можете видеть, AddComponent добавляет к частной _components переменной. Но единственный способ проверить, что это происходит, — использовать индексатор. Это нормально, но для того, чтобы протестировать индексатор, мне тоже пришлось бы вызвать AddComponent !

Другими словами, в модульных тестах для индексатора и AddComponent каждый тест должен был бы вызывать оба этих метода. Похоже, это создает ненужную связь. Если в индексаторе есть ошибка, нет причин для моего TestAddComponent сбоя.

Какова наилучшая практика здесь? Использую ли я отражение для получения _components ? Издевательство? Что-то еще?

Ответ №1:

На мой взгляд, модульные тесты не должны отражать свои цели. Я думаю, что в такого рода тестах оба должны тестироваться вместе, в одном тесте. Но это всего лишь точка зрения.

Однако вы можете выполнить несколько тестов, изменив порядок инструкций. Попробуйте добавить несколько, и доступ к первому, затем к последнему, затем к одному из середины. Каждый тест представляет собой один сценарий с разным порядком и количеством вставок. Вы даже можете протестировать исключительные состояния, которые должны произойти… например, если вы пытаетесь получить что-то, что не было вставлено.

Я думаю, что модульный тест существует для имитации использования или для обеспечения соблюдения спецификации. Не проверять, верен ли каждый отдельный бит программы, потому что это убивает гибкость.

Ответ №2:

Ну, у вас есть 2 варианта:

  1. Используйте отражение или классы MSTest private accessor для получения и установки значений private field во время вашего теста.
  2. Просто не беспокойтесь об этом и протестируйте открытое поведение, даже если это означает, что ваш тест зависит от других свойств или метода, которые тестируются в другом месте.

Как вы, вероятно, можете судить по формулировке, мой выбор был бы с # 2 — Вы должны протестировать открытое поведение. В вашем случае открытое поведение, которое вы тестируете, является:

  • Если я использую AddComponent , то добавленный компонент должен быть доступен через индексатор
  • Если я использую индексатор, я должен иметь доступ ко всем компонентам, которые были добавлены через AddComponent

В данном случае довольно очевидно, что это практически одно и то же, поэтому у нас действительно есть только один единичный случай / открытое поведение для тестирования здесь. Да, этот модульный тест охватывает две разные вещи, но это не должно иметь большого значения — мы не пытаемся проверить, что каждый метод / свойство ведет себя так, как ожидалось, скорее мы хотим проверить, что каждое отображаемое поведение работает так, как ожидалось.


В качестве альтернативы предположим, что мы выбираем вариант 1 и используем частное отражение для проверки _components собственного состояния. В этом случае время, которое мы фактически тестируем, составляет:

  • Если я использую AddComponent , то добавленный компонент должен быть добавлен в _components
  • Если я использую индексатор, я должен иметь доступ ко всем компонентам, которые находятся в _components

Сейчас мы не только тестируем внутреннее поведение класса (чтобы при изменении реализации тесты завершались неудачей, даже если класс работает должным образом), но мы только что удвоили количество тестов, которые мы пишем.

Кроме того, увеличивая сложность наших тестов, мы увеличиваем вероятность того, что в самих тестах есть ошибка — например, что, если мы допустили ошибку и в тесте 2. мы проверили какое-то совершенно другое закрытое поле? В этом случае мы не только сделали больше работы для себя, но мы даже не тестируем реальное поведение, которое хотим протестировать!

Ответ №3:

Когда вы используете Microsoft Unit Test Framework, платформа генерирует частный класс доступа. Это должно позволить вам получить доступ к вашим частным типам. Взгляните на эту страницу от Microsoft для получения дополнительной информации:

http://msdn.microsoft.com/en-us/library/dd293546.aspx

Специально для этого раздела: Создавайте модульные тесты, которые могут получать доступ к внутренним, частным и дружественным методам.

Ответ №4:

Могу ли я предложить использовать интерфейсы и / или виртуальные методы и MOQ. Таким образом, вы можете MOQ вызовы методов, которые вы не хотите тестировать, и заставить их возвращать то, что вы хотите.

Ответ №5:

Здесь есть два варианта.

  1. Протестируйте функциональность вместе, как уже упоминалось.
  2. Измените свой класс, чтобы вы могли обернуть его в тестовый жгут.

Ваш тестовый набор должен быть определен с помощью ваших модульных тестов и предоставлять элементы, которые вам нужны для проверки правильности работы функциональности. Вы должны использовать свой тестовый жгут вместо класса непосредственно в своих модульных тестах.

 
public class MyClass
{    
    protected readonly Dictionary<Type, Component> _components = new Dictionary<Type, Component>();

    public Component this [Type type]
    {
        get
        {
            Component component;
            _components.TryGetValue(type, out component);
            return component;
        }
    }

    public void AddComponent(Component component)
    {
        _components.Add(component.GetType(), component);
    }
}

public class MyClassTestHarness : MyClass
{
    public Dictionary<Type, Component> Components
    {
        get
        {
            return _components;
        }
    }
}

  

Забыл упомянуть другой вариант, который заключается в внедрении зависимостей с помощью mocking. Если бы вы использовали макет IDictionary, вы могли бы проверить свой тест.

 
public class MyClass
{    
    protected IDictionary _components;

    public MyClass()
    {
         _components = new Dictionary();
    }

    public MyClass(IDictionary components)
    {
         _components = components;
    }

    public Component this [Type type]
    {
        get
        {
            Component component;
            _components.TryGetValue(type, out component);
            return component;
        }
    }

    public void AddComponent(Component component)
    {
        _components.Add(component.GetType(), component);
    }
}
  

Ответ №6:

Если вы хотите сделать это таким образом, перейдите из класса в конструктор (на данный момент игнорируя фреймворки IoC) и используйте интерфейс для ссылки на зависимость:

 class ComponentManager
{
    private readonly IDictionary<Type, Component> _components;

    public ComponentManager()
        : this(new Dictionary<Type, Component>())
    { }

    public ComponentManager(IDictionary<Type, Component> components)
    {
        _components = components;
    }

    public Component this[Type type]
    {
        get
        {
            Component component;
            _components.TryGetValue(type, out component);
            return component;
        }
    }

    public void AddComponent(Component component)
    {
        _components.Add(component.GetType(), component);
    }
}
  

Теперь вы можете имитировать зависимость и проверять взаимодействия.

Однако, учитывая отсутствие добавленного поведения, я считаю, что по-настоящему практичным подходом является прямое представление элемента и удаление средств доступа к агрегированному объекту:

 class ComponentManager
{
    public Dictionary<Type, Component> Components { get; private set; }

    public ComponentManager()
    {
        Components = new Dictionary<Type, Component>();
    }
}