Как выполнить модульное тестирование класса с неприятными зависимостями без фиктивного фреймворка?

Я работаю с устаревшей кодовой базой C++ и хочу протестировать некоторые методы класса DependsOnUgly, у которого есть зависимость, которую нелегко сломать в большом классе (Ugly) с большим количеством внешних зависимостей от файловой системы и т. д. Я хочу получить хотя бы некоторые тестируемые методы DependsOnUgly, при этом как можно меньше модифицируя существующий код. Невозможно создать шов фабричным методом, параметром метода или параметром конструктора без большого количества модификаций кода; Ugly - это конкретный класс, от которого напрямую зависит без какого-либо абстрактного базового класса, и он имеет большое количество методов, некоторые из которых или ни один из которых не помечены virtual, поэтому полное издевательство над ним было бы очень утомительным. У меня нет фиктивной среды, но я хочу протестировать DependsOnUgly, чтобы внести изменения. Как я могу сломать внешние зависимости Ugly для модульного тестирования методов на DependsOnUgly?


person Keith Pinson    schedule 15.04.2013    source источник


Ответы (1)


Используйте то, что я называю препроцессорным макетом, макет, введенный через шов препроцессора.

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

Вот условные реализации Ugly и NotAsUgly для примера.

DependsOnUgly.hpp

#ifndef _DEPENDS_ON_UGLY_HPP_
#define _DEPENDS_ON_UGLY_HPP_
#include <string>
#include "Ugly.hpp"
class DependsOnUgly {
public:
    std::string getDescription() {
        return "Depends on " + Ugly().getName();
    }
};
#endif

Уродливый.hpp

#ifndef _UGLY_HPP_
#define _UGLY_HPP_
struct Ugly {
    double a, b, ..., z;
    void extraneousFunction { ... }
    std::string getName() { return "Ugly"; }
};
#endif

Есть две основные вариации. Во-первых, только определенные методы Ugly вызываются DependsOnUgly, и вы уже хотите издеваться над этими методами. Второй

Метод 1: заменить все поведение Ugly, используемое DependsOnUgly

Я называю этот метод Частичная имитация препроцессора, поскольку макет реализует только необходимые части интерфейса моделируемого класса. Используйте охранники включения с тем же именем, что и у рабочего класса, в файле заголовка для фиктивного класса, чтобы производственный класс никогда не определялся, а был фиктивным. Не забудьте включить макет перед DependsOnUgly.hpp.

(Обратите внимание, что мои примеры тестового файла не являются самопроверяемыми; это просто для простоты и для того, чтобы не зависеть от среды модульного тестирования. Основное внимание уделяется директивам в верхней части файла, а не самому фактическому методу тестирования. .)

test.cpp

#include <iostream>
#include "NotAsUgly.hpp"
#include "DependsOnUgly.hpp"
int main() {
    std::cout << DependsOnUgly().getDescription() << std::endl;
}

NotAsUgly.hpp

#ifndef _UGLY_HPP_ // Same name as in Ugly.hpp---deliberately!
#define _UGLY_HPP_
struct Ugly { // Once again, duplicate name is deliberate
    std::string getName() { return "not as ugly"; } // All that DependsOnUgly depends on
};
#endif

Техника 2: Замените часть поведения Ugly, используемую DependsOnUgly

Я называю это мока-подклассом на месте, потому что в этом случае Ugly является подклассом и необходимые методы переопределяются, в то время как другие все еще доступны для использования, но имя подкласса по-прежнему Ugly. Директива определения используется для переименования Ugly в BaseUgly; затем используется директива undefine и фиктивные Ugly подклассы BaseUgly. Обратите внимание, что для этого может потребоваться пометить что-то в Ugly как виртуальное в зависимости от конкретной ситуации.

test.cpp

#include <iostream>
#define Ugly BaseUgly
#include "Ugly.hpp"
#undef Ugly
#include "NotAsUgly.hpp"
#include "DependsOnUgly.hpp"
int main() {
    std::cout << DependsOnUgly().getDescription() << std::endl;
}

NotAsUgly.hpp

#ifndef _UGLY_HPP_ // Same name as in Ugly.hpp---deliberately!
#define _UGLY_HPP_
struct Ugly: public BaseUgly { // Once again, duplicate name is deliberate
    std::string getName() { return "not as ugly"; }
};
#endif

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

person Keith Pinson    schedule 15.04.2013
comment
Потрясающая техника! +1 - person Fabi1816; 16.04.2015
comment
Как в методе 2 может быть выполнен NatAsUgly.hpp? Когда Ugly.hpp включен #include в test.cpp, устанавливается значение _UGLY_PP. Таким образом, #ifndef в строке 1 в NotAsUgly.hpp должно оцениваться как false и должно произойти другое. - person wesanyer; 22.07.2017
comment
@Keith Pinson, для метода 1 вам нужно создать несколько проектов (которые будут использовать разные make-файлы для создания разных исполняемых файлов) для ваших модульных тестов, верно? Например, если вы хотите сделать тест, который тестирует Ugly, вам понадобится новый проект, который не компилирует NotAsUgly. В противном случае вы получите несколько ошибок определения, верно? - person Programmer_D; 07.09.2018
comment
@Programmer_D звучит правильно, но мне не приходилось использовать эти методы с тех пор, как я впервые ответил на этот вопрос, поэтому я не помню деталей. - person Keith Pinson; 07.09.2018