Подтвердить что ты не робот

Частичные Mocks плохие, почему именно?

Сценарий

У меня есть вызов класса 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 stubbing/mocking все зависимости для calculateComplexMetric() создавали бы очень большой и трудный для поддержания теста (более 100 строк кода установки для проверки разумного репрезентативного сценария, а я необходимо протестировать довольно много сценариев).

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

Однако в документации Mockito указано, что частичные mocks - это запах кода.

Вопрос

Лично я бы разрезал этот угол и просто частично высмеял класс Metrics. Но мне интересно узнать, что такое "пуристский" способ структурирования этого кода и unit test?

Решение

В итоге я разделился на более мелкие, более сплоченные классы, как это было предложено принятым ответом, и используя инъекцию зависимостей:

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 реализовать кэширование, если им необходимо повысить производительность на более позднем этапе. Я считаю, что добавление метрик к конструктору приемлемо по сравнению с передачей их вместе с функцией расчета в каждом вызове. Даже это может быть скрыто с помощью метода factory.

4b9b3361

Ответ 1

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

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

Ваш класс Metric делает больше, чем одно: он вычисляет различные типы показателей. Рассмотрим рефакторинг его в абстрактный Metric и соответствующие подклассы - каждый рассчитывает свою собственную метрику.

Некоторые аргументы для полезности частичных mocks можно найти здесь. Однако все они основаны на сценарии, когда SRP уже нарушен, и вы ничего не можете с этим поделать.

Ответ 2

В связи с неизбежной сложностью Stubbing/mocking класса Model все зависимости для calculateComplexMetric() создавали бы очень большой и сложный для поддержания теста (более 100 строк кода установки для проверьте разумный репрезентативный сценарий, и мне нужно протестировать довольно несколько сценариев).

Кто скажет, что вам все равно нужно mock/заглушить все зависимости для calculateComplexMetric()? Вы спрашиваете о пуристском способе проверить это. Если вы не имеете в виду мокалистский способ, то это было бы, высмеивая "порты" (UI, DB и т.д.) На ваш основной код приложения и модуль, тестируя несколько классов вместе. Это может решить вашу проблему, если ваше приложение уже имеет код для создания реального Model на основе меньшего количества настроек. Я бы предпочел пойти туда, если это возможно.

В противном случае, и если вы хотите сохранить эти методы вместе в этом классе (что может иметь смысл), вы можете сделать что-то, что, вероятно, против всех советов (и, конечно же, не пуристов), но также может быть прекрасно если calculateComplexMetric() можно реализовать полностью без гражданства, учитывая результаты простых показателей: создайте метод static (может быть общедоступным или даже просто видимым в пакете и не должен находиться в том же классе), который реализует вычисление с помощью простых показателей как параметров. Это должно сделать его очень легким для тестирования. Оригинальный calculateComplexMetric() просто вызывает простые методы и делегирует статический метод. Его реализация может быть настолько простой, что ее не нужно явно тестировать изолированно.

"Недостатком" этого является то, что статический метод сложнее высмеять. Но это не обязательно, поскольку оно не пересекает какой-либо порт (например, доступ к БД или вызов веб-службы). Но в любом случае, это, вероятно, так далеко от пуриста, как вы можете получить.

Наконец, вместо статического метода вы можете добавить его как обычный метод в свой класс Metrics. Вы даже можете сделать его закрытым, если вы не хотите его раскрывать, и ваш тест находится в одном пакете.

Ответ 3

Сколько методов calculateSimpleMetricX мы говорим? Если это всего лишь несколько, возможно, стоит рассмотреть просто передачу возвращаемых значений простых показателей в качестве параметров calculateComplexMetric

public double calculateComplexMetric(double simple1, double simple2, ...)

Если слишком много параметров, возможно, стоит инкапсулировать значения простых метрик в новый класс SimpleMetrics и передать это вместо:

public double calculateComplexMetric(SimpleMetrics metrics){
    double sm1 = metrics.get1();
    double sm2 = metrics.get2();
    ...
}

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