dock: простая библиотека модульного тестирования кода на С++

в 12:39, , рубрики: c++, c++ библиотеки, C++14, unit-testing, Unit-тестирование, Тестирование IT-систем

Хотя и существуют уже библиотеки для юнит-тестирования кода на С++, например, Google Test или Bandit, но они написаны не мной здесь оно, на мой взгляд, как-то переусложнено, по сравнению с тем же JS. Там просто делаешь, например, npm i mocha assert --save-dev и можно приступать к написанию тестов, а здесь же нужно это сделать ручками, а в случае с gtest еще и собрать с помощью cmake ее. Bandit подключается просто, но не умеет в сериализацию результатов в какой-то формат данных, gtest это умеет, но его нужно собирать отдельно. А я не хочу выбирать "либо то, либо это". Мне было нужно сделать удобный и простой инструмент под мои задачи. Я хотел получить простую библиотеку без зависимостей, header-only, на несколько файлов, которую можно легко и быстро подключить к своему проекту, удобно внести в нее изменения (если это будет необходимо). Но, самое основное, мне хотелось получать удобные, машиночитаемые отчеты, причем не только в stdout (или xml, как в gtest), но и в любой другой формат, который я захочу. Далее под катом.

Как я уже писал выше, библиотека dock header-only, а значит ее подключение максимально простое:

#include <iostream>
#include <dock/dock.hpp>

using namespace dock;

int main() {
    core().run();

    return 0;
}

При сборке, например, в gcc, нужно передать только путь к папке с библиотеками и указать стандарт языка C++14. Я намеренно делаю так, потому что новые проекты я пишу на свежем стандарте, а для поддержки старых есть уже свои готовые библиотеки.

Описание тестов тоже сделано предельно простым:

using namespace dock;

Module(u8"Some module 1", [](DOCK_MODULE()) {
    Test(u8"Some test 1", []() {
        Assert::isTrue([]() -> bool { return true; });
    });

    Test(u8"Some test 2", []() {
        Assert::isTrue([]() -> bool { return true; });
    });
});

Module(u8"Some module 2", [](DOCK_MODULE()) {
    Test(u8"Some test 1", []() {
        Assert::isTrue([]() -> bool { return true; });
    });

    Test(u8"Some test 2", []() {
        Assert::isTrue([]() -> bool { return false; });
    });
});

Для удобства тесты группируются в модули. В них передается объект std::function<void(Module*)>, внутри которого описываются непосредственно тесты. Тесты имеют примерно такой же синтаксис, только функциональный объект без параметров. Пока что я не делал проверку на уникальность имени модуля или теста, потому что это было не критично.

"Библиотека" Assert содержит простой набор методов isTrue,isEquals, isGreater, isLess, которые по умолчанию могут сравнивать объекты через операторы ==, > или <. Если операторов нет, то можно функцию сравнения передать в конце параметром (например, в виде лямбды).

static void isTrue(std::function<bool()> fcn);

template<typename T>
static void isEquals(const T a, const T b, std::function<bool(const T, const T)> compareFcn = defaultEqualsFunction<T>);

template<typename T>
static void isGreater(const T a, const T b, std::function<bool(const T, const T)> compareFcn = defaultGreaterFunction<T>);

template<typename T>
static void isLess(const T a, const T b, std::function<bool(const T, const T)> compareFcn = defaultLessFunction<T>);

А теперь как раз то, что было нужно мне: удобное преобразование результатов тестирования в необходимый формат данных. Для начала, просто хочется поработать с статистикой ведения проекта, смотреть динамику по тестам и подобные вещи, и мне это удобно делать на JS. Поэтому первый формат, который мне потребовался — JSON. В репозитории есть уже три готовых сериализатора: в JSON, в plain text и вывод в консоль с подсветкой. Использование сериализаторов очень простое:

nlohmann::json outJson;
JsonSerializer serializer(outJson, 4);

core().run();
core().collect(serializer);

std::cout << serializer << std::endl;

А сам интерфейс сериализатора выглядит следующим образом:

class ResultSerializer {
public:
    virtual ~ResultSerializer() = default;

    virtual void            serialize(std::vector<Result>& results) = 0;
    virtual std::string     toString() const = 0;
    friend std::ostream&    operator<<(std::ostream& os, ResultSerializer& s);
};

Т.е. выводить результат можем куда угодно, подставить только std::ostream и все. Логика работы сериализатора следующая:

  • Передаем сериализатор движку через collect() и он вызывает метод serialize() с вектором результатов.
  • В операторе << вызывается метод toString(), который выдает строку в std::ostream.
    Можно сделать два варианта: либо при вызове serialize() сразу создаем нужную строку, а ее потом либо просто возвращаем, либо сохраняем ссылку на результаты и генерируем выдачу непосредственно при выдаче в ostream. В любом случае, остается свобода движения — движок выдает просто std::vector<dock::Result>, а что с ним делать уже дело ваше :).

Лицензия свободная (MIT), потому что мне не жалко и будет приятно видеть её использование. Для сериализаторов использовались библиотеки termcolor и JSON for Modern C++, но можно спокойно убрать их вместе с ненужными сериализаторами.

Автор: emdc

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js