Снять муфту, а затем макет для модульного теста
Это дилемма. Скажем, у нас есть два класса
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 варианта, я думаю:
Введите А в конструктор
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()); }
- Проходить
A
к методу, если это то, чтоB
нужно работать (как здесь кажется). Вы все еще можете создать макет версии для использования в ваших тестах. Это тот же код, что и выше, ноmock
передается в метод вместо конструктора. Это лучший метод, еслиA
это некоторый класс данных, сгенерированный во время выполнения вместо класса с поведением. Создать фабричный класс для
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
заявление.
Но все зависит от контекста и размера вашего проекта.