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


35

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

Это очень упрощенный пример. На самом деле функции намного сложнее.

Пример:

public class MyClass
{
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionB()
     {
         return new Random().Next();
     }
}

Таким образом, чтобы проверить это у нас есть 2 метода.

Метод 1: Используйте функции и действия, чтобы заменить функциональность методов. Пример:

public class MyClass
{
     public Func<int> FunctionB { get; set; }

     public MyClass()
     {
         FunctionB = FunctionBImpl;
     }

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionBImpl()
     {
         return new Random().Next();
     }
}

[TestClass]
public class MyClassTests
{
    private MyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new MyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionB = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

Метод 2: Сделайте члены виртуальными, производным классом и в производном классе используйте Функции и Действия для замены функциональности. Пример:

public class MyClass
{     
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected virtual int FunctionB()
     {
         return new Random().Next();
     }
}

public class TestableMyClass
{
     public Func<int> FunctionBFunc { get; set; }

     public MyClass()
     {
         FunctionBFunc = base.FunctionB;
     }

     protected override int FunctionB()
     {
         return FunctionBFunc();
     }
}

[TestClass]
public class MyClassTests
{
    private TestableMyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new TestableMyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionBFunc = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

Я хочу знать, что лучше, а также почему?

Обновление: ПРИМЕЧАНИЕ: функция B также может быть общедоступной


Ваш пример прост, но не совсем корректен. FunctionAвозвращает bool, но устанавливает только локальную переменную xи ничего не возвращает.
Эрик П.

1
В этом конкретном примере FunctionB может быть, public staticно в другом классе.

Ожидается, что для Code Review вы опубликуете реальный код, а не его упрощенную версию. Смотрите FAQ. В настоящее время вы задаете конкретный вопрос, а не ищете обзор кода.
Уинстон Эверт

1
FunctionBсломан по дизайну. new Random().Next()почти всегда неправильно. Вы должны ввести экземпляр Random. ( Randomтакже плохо спроектированный класс, который может вызвать несколько дополнительных проблем)
CodesInChaos

На более общей ноте DI через делегатов - это совершенно нормально imho
jk.

Ответы:


32

Отредактировано после оригинального обновления постера.

Отказ от ответственности: не программист C # (в основном Java или Ruby). Мой ответ будет таким: я бы вообще не проверял это, и я не думаю, что вы должны это делать.

Более длинная версия: частные / защищенные методы не являются частью API, они в основном являются вариантами реализации, которые вы можете решить просмотреть, обновить или полностью исключить, не оказывая никакого влияния извне.

Я полагаю, у вас есть тест для FunctionA (), который является частью класса, видимого из внешнего мира. Он должен быть единственным, у которого есть контракт на реализацию (и который можно было бы проверить). Ваш частный / защищенный метод не имеет контракта для выполнения и / или тестирования.

См. Соответствующее обсуждение там: https://stackoverflow.com/questions/105007/should-i-test-private-methods-or-only-public-ones

После комментария , если FunctionB общедоступен, я просто протестирую оба, используя модульный тест. Вы можете подумать, что тест FunctionA не является полностью «модульным» (так как он вызывает FunctionB), но меня это не слишком беспокоит: если тест FunctionB работает, но не тест FunctionA, это означает, что проблема не в поддомен FunctionB, что достаточно для меня как дискриминатора.

Если вы действительно хотите иметь возможность полностью разделить два теста, я бы использовал некоторую технику насмешки, чтобы смоделировать FunctionB при тестировании FunctionA (как правило, возвращать фиксированное известное правильное значение). Мне не хватает знаний об экосистеме C #, чтобы посоветовать конкретную библиотеку-макет, но вы можете посмотреть на этот вопрос .


2
Полностью согласен с ответом @Martin. Когда вы пишете модульные тесты для класса, вы не должны тестировать методы . То, что вы тестируете, - это поведение класса , то, что контракт (объявление того, что должен делать класс) выполняется. Итак, ваши модульные тесты должны охватывать все требования, предъявляемые к этому классу (с использованием открытых методов / свойств), включая исключительные случаи

Здравствуйте, спасибо за ответ, но он не ответил на мой вопрос. Мне не важно, был ли FunctionB закрытым / защищенным. Он также может быть общедоступным и все еще вызываться из FunctionA.

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

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

2
Тот факт, что FunctionA вызывает FunctionB, не имеет значения с точки зрения модульного тестирования. Если тесты для FunctionA написаны правильно, это детали реализации, которые могут быть реорганизованы позже без прерывания тестов (при условии, что общее поведение FunctionA останется неизменным). Реальная проблема заключается в том, что функция FunctionB извлекает случайное число с помощью внедренного объекта, так что вы можете использовать макет во время тестирования, чтобы гарантировать, что известное число будет возвращено. Это позволяет вам тестировать известные входы / выходы.
Дэн Лайонс

11

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

Так что, если я нахожусь в сценарии, где у меня есть

class A 
{
     public B C()
     {
         D();
     }

     private E D();
     {
         // i actually want to control what this produces when I test C()
         // or this is important enough to test on its own
         // and, typically, both of the above
     }
}

Тогда я собираюсь рефакторинг.

class A 
{
     ICollaborator collaborator;

     public A(ICollaborator collaborator)
     {
         this.collaborator = collaborator;
     }

     public B C()
     {
         collaborator.D();
     }
}

Теперь у меня есть сценарий, в котором D () независимо тестируется и полностью заменяется.

Как средство организации, мой соавтор может не жить на том же уровне пространства имен. Например, если Aесть в FooCorp.BLL, то мой соавтор может быть другого уровня, как в FooCorp.BLL.Collaborators (или как угодно подходящее имя). Кроме того, мой соавтор может быть виден только внутри сборки через internalмодификатор доступа, который я затем также предоставлю для моего проекта модульного тестирования через InternalsVisibleToатрибут сборки. Вывод заключается в том, что вы можете поддерживать чистоту своего API в отношении вызывающих абонентов, создавая проверяемый код.


Да, если ICollaborator требуется несколько методов. Если у вас есть объект, единственная задача которого - обернуть единственный метод, я бы предпочел, чтобы он был заменен делегатом.
JK.

Вы должны решить, имеет ли смысл именованный делегат или интерфейс имеет смысл, и я не буду решать за вас. Лично я не против классов с одним (публичным) методом. Чем меньше, тем лучше, так как их становится все легче понять.
Энтони Пеграм,

0

Добавляя к тому, на что указывает Мартин,

Если ваш метод закрыт / защищен - не проверяйте его. Он является внутренним для класса и не должен быть доступен вне класса.

В обоих подходах, которые вы упоминаете, у меня есть эти проблемы -

Метод 1 - Это фактически изменяет поведение тестируемого класса в тесте.

Метод 2 - Это на самом деле не тестирует производственный код, а тестирует другую реализацию.

В поставленной задаче я вижу, что единственная логика A - видеть, является ли выход FunctionB четным. Несмотря на иллюстрацию, FunctionB дает значение Random, которое сложно протестировать.

Я ожидаю реалистичного сценария, в котором мы можем настроить MyClass таким образом, чтобы мы знали, что вернет FunctionB. Тогда наш ожидаемый результат известен, мы можем вызвать FunctionA и подтвердить фактический результат.


3
protectedпочти так же, как public. Только privateи internalесть детали реализации.
CodesInChaos

@codeinchaos - мне здесь любопытно. Для теста защищенные методы являются «частными», если вы не измените атрибуты сборки. Только производные типы имеют доступ к защищенным членам. За исключением виртуальных, я не понимаю, почему защищенные должны рассматриваться как тестовые, как общедоступные. не могли бы вы уточнить, пожалуйста?
Срикант Венугопалан

Поскольку эти производные классы могут находиться в разных сборках, они подвергаются воздействию стороннего кода и, таким образом, являются частью общедоступной поверхности вашего класса. Чтобы проверить их, вы можете сделать их internal protected, использовать частный помощник отражения или создать производный класс в своем тестовом проекте.
CodesInChaos

@CodesInChaos согласился с тем, что производный класс может находиться в разных сборках, но область действия по-прежнему ограничена базовым и производными типами. Я немного нервничаю из-за того, что модифицирую модификатор доступа, чтобы сделать его тестируемым. Я сделал это, но мне кажется, что это антипаттерн.
Срикант Венугопалан

0

Я лично использую Method1, то есть превращаю все методы в Actions или Funcs, поскольку это значительно улучшило тестируемость кода для меня. Как и у любого решения, у этого подхода есть свои плюсы и минусы:

Pros

  1. Позволяет простую структуру кода, где использование шаблонов кода только для модульного тестирования может добавить дополнительную сложность.
  2. Позволяет запечатывать классы и исключать виртуальные методы, которые требуются популярным фреймворкам Mock. Запечатывание классов и исключение виртуальных методов делает их кандидатами для встраивания и других оптимизаций компилятора. ( https://msdn.microsoft.com/en-us/library/ff647802.aspx )
  3. Упрощает тестируемость, поскольку заменить реализацию Func / Action в модульном тесте так же просто, как просто присвоить новое значение Func / Action
  4. Также позволяет проверить, был ли статический Func вызван из другого метода, поскольку статические методы не могут быть смоделированы.
  5. Легко реорганизовать существующие методы в Funcs / Actions, поскольку синтаксис вызова метода на сайте вызова остается прежним. (Если вы не можете преобразовать метод в Func / Action, см. «Против»).

Cons

  1. Funcs / Actions нельзя использовать, если ваш класс может быть получен, так как Funcs / Actions не имеют путей наследования, таких как Methods
  2. Невозможно использовать параметры по умолчанию. Создание Func с параметрами по умолчанию влечет за собой создание нового делегата, что может привести к путанице в коде в зависимости от варианта использования
  3. Невозможно использовать синтаксис именованного параметра для вызова методов вроде чего-либо (firstName: "S", lastName: "K")
  4. Самым большим недостатком является то, что у вас нет доступа к ссылке «this» в функциях и действиях, и поэтому любые зависимости от класса должны явно передаваться в качестве параметров. Это хорошо, потому что вы знаете все зависимости, но плохо, если у вас есть много свойств, от которых будет зависеть ваш Func. Ваш пробег будет варьироваться в зависимости от вашего варианта использования.

Итак, подведем итог: использование Funcs и Actions for Unit test - это замечательно, если вы знаете, что ваши классы никогда не будут переопределены.

Кроме того, я обычно не создаю свойства для функций, а вставляю их непосредственно как таковые

public class MyClass
{
     public Func<int> FunctionB = () => new Random().Next();

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }
}

Надеюсь это поможет!


-1

Использование Mock возможно. Nuget: https://www.nuget.org/packages/moq/

И поверьте мне, это довольно просто и имеет смысл.

public class SomeClass
{
    public SomeClass(int a) { }

    public void A()
    {
        B();
    }

    public virtual void B()
    {

    }
}

[TestFixture]
public class Test
{
    [Test]
    public void Test_A_Calls_B()
    {
        var mockedObject = new Mock<SomeClass>(5); // You can also specify constructor arguments.
        //You can also setup what a function can return.
        var obj = mockedObject.Object;
        obj.A();

        Mock.Get(obj).Verify(x=>x.B(),Times.AtLeastOnce);//This test passes
    }
}

Для макета нужен виртуальный метод для переопределения.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.