Как частично переопределить некоторые методы класса для тестирования в C#, сохранив при этом исходное определение класса нетронутым?

#c# #testing #mocking

Вопрос:

Проблема

Мы хотели бы найти способ выборочно переопределять некоторые методы класса, но не изменять немного его определения.
Кроме того, мы хотели бы, чтобы переопределение было необязательным (т. е. не обязательным). То есть мы можем переопределить его, когда захотим, но мы также можем отказаться от написания какой-либо строки кода для него, когда мы этого не хотим.

Пример использования

Предположим, у нас есть класс, предоставляющий данные, назовем его Model .
У нас есть другой класс, который использует эти данные. Это View .

Теперь мы хотели бы провести интеграционные тесты для View . Для удобства мы хотели бы изменить некоторую часть предоставленных данных Model , оставив другую часть как есть.
Поскольку мы хотим изменить возвращаемые данные Model , это означает, что мы должны переопределить некоторые из их поведения (т. е. методы). Однако, поскольку это всего лишь тест, естественно, мы не хотим изменять исходный код Model .

Предыдущие Попытки

Оформитель

Первый подход , который я мог придумать Model , — это, скажем IModel , извлечение интерфейса и создание для него декоратора, такого как

 public interface IModel
{
    string GetSomething();
    string GetOther(); // We want to override this
}

public class Model : IModel // This is the real model
{
}

public class ModelWrapper : IModel
{
    private IModel _internal; // Should be an instance of Model

    public string GetSomething() => _internal.GetSomething(); // we want to keep the behavior of original model for this method

    public string GetOther() // We want to change the behavior of this one
    {
        return "something for testing";
    }
}
 

Преимущество такого подхода заключается в том, что мы будем иметь полный контроль над макетом.
Однако недостатком было бы то, что мы должны продолжать поддерживать ModelWrapper .
Когда IModel что-то меняется, мы вынуждены обновлять ModelWrapper .
К сожалению, их у нас много IModel , и они часто меняются. Поддержание этих классов-оболочек довольно тревожно.

Издевательская библиотека

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

Нам не нужно указывать поведение каждого метода интерфейса. Мы можем издеваться над ними выборочно.
Поведение разблокируемых методов автоматически определяется макетной библиотекой. Обычно они просто возвращают значение по умолчанию возвращаемого типа.

Однако значения по умолчанию иногда могут быть недостаточно хороши для нас, потому что нам может понадобиться, чтобы они находились в каком-то определенном диапазоне, или IView это будет выглядеть странно. Лучшим резервным вариантом может быть использование наших (существующих) конкретных классов. Обычно они дают правильные результаты.

До сих пор я не могу найти издевательскую библиотеку, которая могла бы вернуться к конкретному классу. NSubstitute и Moq поддерживают следующие две вещи, но они не совсем идеальны:

  1. Макет интерфейса. Методы, над которыми явно не издеваются, возвращают значения по умолчанию. Вид может показаться странным.
  2. Поиздеваться над классом. Переопределить можно только виртуальные методы. Нам нужно изменить код наших конкретных классов, если мы хотим издеваться над ними таким образом.

Каковы другие хорошие способы достижения этой цели?
Или я решаю не ту проблему? Может быть, есть какие-то другие методы для удовлетворения таких потребностей в тестировании?

Комментарии:

1. Почему вы хотите переопределить части модели? Не могли бы вы описать пример использования? Обычно при проведении интеграционного тестирования вы используете компоненты как есть или имитируете целые компоненты, такие как база данных.

2. Пример использования заключается в том, что мы работаем над графическими интерфейсами и хотим отшлифовать и протестировать состояния каждой «страницы» из них отдельно. Например, давайте предположим, что мы создаем StackOverflow и работаем над страницей списка вопросов. Он может использовать модель учетной записи, которая содержит профиль пользователя и список вопросов, которые пользователь задал/ответил. Когда мы полируем страницу, нам не нужно устанавливать для части профиля определенные значения. Но это должно быть «нормально», так как на странице отображается информация о пользователе. С другой стороны, мы можем захотеть настроить часть списка вопросов для быстрого тестирования различных случаев.

Ответ №1:

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

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

Комментарии:

1. Спасибо. Это действительно жизнеспособное решение. Однако виртуалы могут вводить в заблуждение других разработчиков. На самом деле мы не хотим, чтобы методы заменялись в производственном коде, но для тестирования требуются виртуалы…

2. Затем эти виртуальные методы могут быть «украшены» атрибутом ConditionalAttribute(«ОТЛАДКА»), чтобы он отфильтровывался и существовал только при отладке, а не в производстве.

3. Условный атрибут-это круто! Я об этом не думал. Но если я использую атрибут ConditionalAttribute, виртуальные методы будут только отлаживаться, верно? То есть его вызывающий объект (IView) также должен быть только отладочным. По крайней мере, IView будет вести себя по-другому в режиме отладки и производства. Разве это не нарушило бы цель насмешек?

4. На самом деле нет, если вы определяете символ с помощью #define и устанавливаете аргумент в качестве условного атрибута для имени символа, это проверит его, и когда вы его распространяете, вы можете просто отменить его определение или прокомментировать #define