Бесплатные функции и модульное тестирование

Я заинтересован в тестировании функции navigationFoo ниже:

virtual void navigateFoo(const vector<Node>& nodes) 
{
    // find the foo node in the list of nodes
    Nodes::const_iterator fooNodeI = findFooNode(nodes);

    // if we have found the foo node
    if(fooNodeI!=nodes.end()) 
    {
        // the id for retrieving the associated Foo Container from the cache 
        std::string id = getCacheIdentifier(*fooNodeI);
        // the Foo Container associated with the Foo Node
        FooContainer& container = _fooContainerCache->get(id);

        // the name of the Foo item within the Foo Container
        std::string name = getName(*fooNodeI);
        // if foo is not found in the associated container, add it
        if(findFoo(name, container)==container.end()) 
        {
            container.push_back( createFoo(getData(*fooNodeI)) );
        }
    }
}

Узел имеет тип boost::variant, где этот вариант содержит типы Foo1, Foo2, Bar1 и Bar2 и т. д.

Бесплатная функция findFooNode использует шаблон посетителя для поиска узла Foo (типа Foo1 или Foo2).

Бесплатная функция getCacheIdentifier также использует шаблон посетителя для поиска идентификатора кеша для узла Foo.

_fooContainerCache — это инъекция зависимостей, над которой издеваются в моем модульном тесте.

getName снова бесплатная функция, как и createFoo.

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

Вещи легко тестируются вплоть до линии:

FooContainer& container = _fooContainerCache->get(id);

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

Однако, чтобы протестировать код после этой строки, мне нужно проверить изменения, внесенные в FooContainer, возвращенные по ссылке из моего макета. Однако, если createFoo изменится в будущем, а я знаю, что это произойдет, это приведет к тому, что мне придется изменить свои модульные тесты как для createFoo, так и для navigationFoo. Однако, если бы вместо этого я должен был внедрить зависимость FooFactory, я бы избежал этой проблемы, выполнив вместо этого следующее:

container.push_back( _fooFactory->create( getData(*fooNodeI) ));

Затем я могу издеваться над этой функцией и в своем модульном тесте. Если поведение этого интерфейса изменится, мне не придется переписывать тесты для navigationFoo.

Однако, когда я писал createFoo, я никогда не считал естественным, что он должен быть реализован как интерфейс, поэтому теперь я чувствую, что добавляю интерфейс просто для того, чтобы иметь возможность писать более качественные тесты. И тогда возникает вопрос, должен ли быть интерфейс для каких-либо других моих бесплатных функций? Есть ли какие-то правила на этот счет?


person Baz    schedule 29.04.2013    source источник
comment
Похоже, вы тестируете на слишком маленьком уровне. Тестирование с таким макетом не имеет большого значения, потому что изменение кода вообще нарушит тест, даже если на клиентах может не быть заметных изменений.   -  person Billy ONeal    schedule 29.04.2013
comment
@Billy ONeal Вы имеете в виду, что если бы вы тестировали приведенную выше строку кода с помощью push_back, вы бы просто проверили, что размер объекта FooContainer увеличился на единицу, и оставили бы его на этом?   -  person Baz    schedule 29.04.2013
comment
@Baz (если возможно) Я бы также как-то проверил его содержимое, а не только размер контейнера.   -  person BЈовић    schedule 29.04.2013
comment
@Billy ONeal Что, если я по ошибке передам *(fooNodeI+1) в getData?   -  person Baz    schedule 29.04.2013
comment
@Baz: я бы проверил все наблюдаемое поведение navigateFoo. Если кто-то совершит ошибку, то это поведение должно быть заметно, поскольку navigateFoo будет давать неправильные ответы.   -  person Billy ONeal    schedule 29.04.2013
comment
@BЈовић Мне также было бы удобнее это делать, но, поскольку я знаю, что поведение create изменится, я чувствую, что сейчас более разумно иметь интерфейс вместо бесплатной функции. Затем я также могу спросить его макет, передан ли ему ожидаемый аргумент.   -  person Baz    schedule 29.04.2013
comment
@Billy ONeal Точно, и поскольку здесь просачивается поведение функции создания, мне придется повторить тесты, которые я уже написал для создания. Вот почему я склонен использовать здесь интерфейс.   -  person Baz    schedule 29.04.2013


Ответы (1)


И тогда возникает вопрос, должен ли быть интерфейс для каких-либо других моих бесплатных функций?

Для SOLID важно, чтобы каждый класс имел интерфейс. Я лично не следую этому правилу на 100%.

Есть ли какие-то правила на этот счет?

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

Однако следите за YAGNI.


Кроме того, этот вопрос может развеять некоторые сомнения.

person BЈовић    schedule 29.04.2013
comment
Это привело бы к множеству инъекций зависимостей в класс, в котором определен navigationFoo, поскольку я не думаю, что какая-либо из этих бесплатных функций будет принадлежать одному и тому же интерфейсу. Означает ли это, что, по вашему мнению, navigationFoo плохо разработан и что его следует разбить на более мелкие части? - person Baz; 29.04.2013
comment
ISP не каждый класс имеет интерфейс. Это означает иметь большое количество маленьких интерфейсов, а не несколько больших. По сути, это SRP для интерфейсов. - person Billy ONeal; 29.04.2013
comment
@BЈовић Кроме того, стандартные алгоритмы часто являются бесплатными функциями и не имеют интерфейсов. - person Baz; 29.04.2013
comment
@Baz Это правда, но они не изменятся. То, что вы хотите издеваться, - это не стандартные функции, но вы хотите сломать свои классы. - person BЈовић; 29.04.2013