Частичные издевательства плохие, почему именно?
сценарий
У меня есть класс, позвоните Model
это представляет собой сложный, составной объект многих других объектов различных типов. Вы можете думать об этом как Car
у которого есть Door[]
, Tire[]
, Engine
, Driver
и т. д. И эти объекты в свою очередь имеют суб-объекты, такие как Engine
имеет SparkPlug
, Clutch
, Generator
и т.п.
у меня есть Metrics
класс, который вычисляет некоторые более или менее сложные метрики о Model
по сути это выглядит примерно так:
public class Metrics{
private final Model model;
public Metrics(Model aModel){model = aModel;}
public double calculateSimpleMetric1(){...}
public double calculateSimpleMetric2(){...}
public double calculateSimpleMetricN(){...}
public double calculateComplexMetric(){
/* Function that uses calls to multiple calculateSimpleMetricX to
calculate a more complex metric. */
}
}
Я уже написал тесты для calculateSimpleMetricX
функции и каждая из них требуют нетривиальных, но управляемых объемов (10-20 строк) кода настройки для правильного макета связанных частей Model
,
проблема
Из-за неизбежной сложности Model
класс заглушки / насмешливые все зависимости для calculateComplexMetric()
создаст очень большой и сложный в обслуживании тест (более 100 строк установочного кода для тестирования разумного репрезентативного сценария, и мне нужно протестировать немало сценариев).
Моя мысль состоит в том, чтобы создать частичный макет Model
и заглушки соответствующие calculateSimpleMetricX()
функции для уменьшения кода настройки до управляемых примерно 10 строк кода. Поскольку эти функции уже проверены отдельно.
Тем не менее, документация Mockito гласит, что частичные макеты являются запахом кода.
Вопрос
Лично я бы здесь срезал угол и просто частично издевался Metrics
учебный класс. Но мне интересно знать, каков "пуристский" способ структурирования этого кода и модульного теста?
Решение
Я закончил тем, что делил на более мелкие, более сплоченные классы в соответствии с принятым ответом и использовал внедрение зависимостей:
public class AbstractMetric{
abstract public double calculate();
}
public class ComplexMetric extends AbstractMetric{
private final SimpleMetric1 sm1;
private final SimpleMetric2 sm2;
public ComplexMetric(SimpleMetric1 asm1, SimpleMetric2 asm2){
sm1 = asm1;
sm2 = asm2;
}
@Ovrerride
public double calculate(){
return functionof(sm1.calculate(), sm2.calculate());
}
}
Это тестируемое, и я могу позволить каждой реализации AbstractMetric
внедрить кэширование, если им нужно улучшить производительность на более позднем этапе. Я считаю, что добавление метрик в конструктор допустимо по сравнению с передачей их функции вычисления при каждом вызове. Даже это можно скрыть с помощью заводского метода.
3 ответа
Я предполагаю, что причина этого может быть следующей: если вам нужно частично насмехаться над классом, чтобы проверить что-то, игнорируя часть его поведения, тогда этот класс делает больше, чем одно. Это нарушает принцип единой ответственности, и это является запахом кода.
Кроме того, если объект может сделать что-нибудь полезное с частично заглушенными внутренними объектами (таким образом, будучи фактически несуществующим), тогда этот объект имеет низкую когезию, что также является запахом кода.
Ваш Metric
класс делает больше, чем одно - он вычисляет различные типы метрик. Рассмотреть вопрос о реорганизации его в абстрактный Metric
и соответствующие подклассы - каждый рассчитывает свою собственную метрику.
Некоторые аргументы в пользу частичной пробной полезности можно найти здесь. Однако все они основаны на сценарии, когда SRP уже нарушен, и вы ничего не можете с этим поделать.
Из-за неизбежной сложности заглушки / насмешки класса Model все зависимости для calcComplexMetric() создают очень большой и сложный в обслуживании тест (более 100 строк установочного кода для тестирования разумного репрезентативного сценария, и мне нужно протестировать довольно несколько сценариев).
Кто сказал, что вам нужно смоделировать / заглушить все зависимости для calculateComplexMetric()
тем не мение? Вы спрашиваете о пуристическом способе проверить это. Если вы не имеете в виду способ mockist, то это было бы путем насмешки "портов" (UI, DB и т. Д.) К вашему базовому коду приложения и модульного тестирования нескольких классов вместе. Это может решить вашу проблему, если ваше приложение уже имеет код для создания реального Model
на основе меньшего количества настроек. Я бы предпочел пойти по этому пути, если это возможно.
В противном случае, и если вы хотите сохранить эти методы вместе в этом классе (что может иметь смысл), вы можете сделать что-то, что, вероятно, противоречит советам всех (и, конечно, не является пуристом), но также может быть вполне приемлемым, если calculateComplexMetric()
может быть реализован полностью без учета состояния с учетом результатов простых метрик: создать static
метод (может быть общедоступным или даже видимым пакетом и не обязательно должен быть в одном классе), который реализует вычисления с простыми метриками в качестве параметров. Это должно облегчить тестирование. Оригинал calculateComplexMetric()
просто вызывает простые методы и делегаты статического метода. Его реализация может быть настолько простой, что не требует явного тестирования в изоляции.
"Недостатком" этого является то, что статический метод труднее подделать. Но в этом нет необходимости, поскольку он не пересекает какой-либо порт (например, обращается к БД или вызывает веб-сервис). Но в любом случае, это, вероятно, далеко от пуриста, как вы можете получить.
Наконец, вместо того, чтобы делать это статическим методом, вы можете добавить его как обычный метод внутри вашего Metrics
учебный класс. Вы даже можете сделать его закрытым, если вы не хотите показывать его, и ваш тест находится в том же пакете.
Как много calculateSimpleMetricX
методы мы говорим? Если это только несколько, возможно, стоит рассмотреть просто передачу возвращенных значений простых метрик в качестве параметровcalculateComplexMetric
public double calculateComplexMetric(double simple1, double simple2, ...)
Если параметров слишком много, возможно, стоит инкапсулировать значения простых метрик в новый класс SimpleMetrics
и передайте это вместо:
public double calculateComplexMetric(SimpleMetrics metrics){
double sm1 = metrics.get1();
double sm2 = metrics.get2();
...
}
В любом случае, теперь легко протестировать вычисление сложных метрик, потому что вы предоставляете значения для простых метрик уже без насмешек.