Снять муфту, а затем макет для модульного теста

Это дилемма. Скажем, у нас есть два класса

Class A
{
    public int memberValue;
}

interface IB
{
    int fun();
}
Class B : IB
{
    public int fun()
    {
        var a = new A();
        switch(a.memberValue)
        {
            case 1:
                //do something
            break;
            case 2:
                //do something
            break;
        }        
    }
}

Теперь это представляет два тесно связанных класса. Для тестирования B.fun () нам нужно смоделировать класс A и предоставить несколько значений для A.memberValue.

Поскольку объект A не требуется где-либо еще за пределами B.fun(), я не понимаю, почему мы должны внедрять его через конструктор B. Как мы можем модульно протестировать этот метод fun()?

2 ответа

Решение

Во-первых, вы, вероятно, должны создать интерфейс для A также, но если это просто простой класс данных POCO, возможно, лучше просто сделать его свойства virtual вместо того, чтобы учесть насмешки. У вас есть 3 варианта, я думаю:

  1. Введите А в конструктор B если это класс, который будет использоваться часто (например, класс журналирования или что-то). Затем вы можете создать пробную версию в своем тесте, чтобы проверить, как A используется (помните, что mocking предназначен для тестирования поведения с зависимостями).

    public class A : IA { ... }
    
    public class B : IB
    {
        private readonly A a;
    
        public B(IA a)
        {
            this.a = a;
        }
    
        public void Func()
        {
            //... use this.a ...
        }
    }
    
    [Test]
    public void Func_AHasValue1_DoesAction1()
    {
        Mock<IA> mock = new Mock<IA>();
        mock.Setup(a => a.somevalue).Returns("something");
    
        B sut = new B(mock.Object);
        sut.Func();
    
        mock.Verify(m => m.SomethingHappenedToMe());
    }
    
  2. Проходить A к методу, если это то, что B нужно работать (как здесь кажется). Вы все еще можете создать макет версии для использования в ваших тестах. Это тот же код, что и выше, но mock передается в метод вместо конструктора. Это лучший метод, если A это некоторый класс данных, сгенерированный во время выполнения вместо класса с поведением.
  3. Создать фабричный класс для A и введите это в конструктор. Измените метод, чтобы получить A экземпляры с фабрики и вставьте фиктивную фабрику в свои тесты, чтобы проверить поведение.

    public class AFactory : IAFactory
    {
        public IA Create() { ... }
    }
    
    public class B : IB
    {
        private readonly IAfactory factory;
    
        public B(IAFactory factory)
        {
            this.factory = factory;
        }
    
        public void Func()
        {
            IA a = factory.Create();
            //... use this.a ...
        }
    }
    
    [Test]
    public void Func_AHasValue1_DoesAction1()
    {
        Mock<IAFactory> mockFactory = new Mock<IAFactory>();
        Mock<IA> mock = new Mock<IA>();
        mockFactory.Setup(f => f.Create()).Returns(mock.Object);
        mock.Setup(a => a.somevalue).Returns("something");
    
        B sut = new B(mockFactory.Object);
        sut.Func();
    
        mock.Verify(m => m.SomethingHappenedToMe());
    }
    
    • Вариант 1 является стандартным подходом для классов, которые могут быть построены без какой-либо информации времени выполнения (например, классы журналирования).
    • Вариант 2 лучше подходит для случаев, когда входными данными является только класс данных, который генерируется во время выполнения (например, пользователь заполняет форму, а у вас есть класс данных POCO, представляющий входные данные формы).
    • Вариант 3 лучше, когда A это то, что имеет поведение, но не может быть создано без чего-то, сгенерированного во время выполнения.

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

Ну, вы можете сделать это несколькими способами. Проще всего будет создать фабричный метод и в тестах создать класс, который наследует B и переопределяет фабричный метод. Я думаю, что это то, что Feathers или Gojko Adzic (я действительно не помню, в чьей книге я читал это, я полагаю, что это была работа Feathers "Эффективно работает с устаревшим кодом") предлагают в таких ситуациях. Пример реализации будет выглядеть примерно так:

class B : IB
{
    public int fun()
    {
        A a = this.CreateA();
        ...
    }
    protected A CreateA() { return new A(); }
}

и в модульном тесте:

class BTest : B
{
    protected override A CreateA() { return mockA(); }
}

Это довольно простое и понятное решение, которое не ограничивает связь на уровне класса, но, по крайней мере, перемещает различные функции в разные методы, поэтому метод, который делает что-то, не заботится о создании объекта.

Но вам нужно тщательно подумать, действительно ли это то, что вы хотите. Для небольшого, короткого проекта это может быть хорошо. Для чего-то большего или долгосрочного, возможно, было бы полезно еще реорганизовать код и удалить зависимость от A от B сделать занятия менее связанными. Кроме того, шаблон, который вы показали, начинает выглядеть Strategy шаблон дизайна. Может быть, вы не должны вводить A но объект стратегии, который будет знать, как справиться с вашей текущей ситуацией? Это всегда то, что приходит мне в голову, когда я вижу if лестница или switch заявление.

Но все зависит от контекста и размера вашего проекта.

Другие вопросы по тегам