Хотя и существуют уже библиотеки для юнит-тестирования кода на С++, например, 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", []() {
uint8_t value = 0x10;
uint8_t expectedValue = 0x10;
Assert::isEqual(value, expectedValue);
});
Test(u8"Some test 2", []() {
uint8_t value = 0x10;
uint8_t expectedBorder = 0x20;
Assert::isLess(value, expectedBorder);
});
});
Module(u8"Some module 2", [](DOCK_MODULE()) {
Test(u8"Some test 1", []() {
Assert::isTrue(true);
});
Test(u8"Some test 2", []() {
Assert::isTrue(false);
});
});
Для удобства тесты группируются в модули. В них передается объект std::function<void(Module*)>
, внутри которого описываются непосредственно тесты. Тесты имеют примерно такой же синтаксис, только функциональный объект без параметров. Пока что я не делал проверку на уникальность имени модуля или теста, потому что это было не критично.
"Библиотека" Assert
содержит простой набор методов isTrue
,isEquals
, isGreater
, isLess
, которые по умолчанию могут сравнивать объекты через операторы ==
, >
или <
. Если операторов нет, то можно функцию сравнения передать в конце параметром (например, в виде лямбды).
static void isTrue(std::function<bool()> fcn);
template<typename T>
static void isEqual(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++, но можно спокойно убрать их вместе с ненужными сериализаторами.
Комментарии (17)
EvilsInterrupt
18.01.2017 17:50+3А чем так Google Test сложен? Ведь достаточно применить один раз применить скрипт:
fuse_gtest_files.py, он создаст 2 файла: cpp, hpp и можно подключать gtest к проекту станет достаточно простым делом.emdc
18.01.2017 20:01+1Да, можно собрать и подключать в проекты, но остается вопрос простого добавления сериализатора, например, в тот же JSON. Здесь же я сделал максимально просто, чтобы можно было выдать данные в каком угодно формате. Можно или только по этой статье написать сразу простейший сериализатор, или заглянуть в один из готовых — там короткие примеры, в которых легко будет и новичку разобраться.
Shamov
18.01.2017 18:06А почему не… ?
std::cout << serializer << core().collect() << std::endl;
emdc
18.01.2017 20:07collect()
принимает сериализатор и говорит ему какой массив нужно преобразовать, поэтому синтаксис выше будет некорректным или просто странноватым. Как вариант, можно задать сериализатор и сделатьstd::cout << core() << std::endl;
, но это, на мой взгляд, как-то не удобно.
Возможно, стоит переименовать метод во что-то более говорящее, но в голову не пришло такое слово.
Shamov
18.01.2017 22:13+2Так меня именно это и удивляет. Почему вдруг
collect()
принимает сериализатор? Гдеcore
и где сериализатор? Зачем они так тесно связаны? Уcore
должен быть некий метод, который отдаёт голенькие данные. И вот эти вот данные уже ловит сериализатор, к которому в результате выполнения первого<<
уже прилипstd::cout
. Т.е. на тот момент, когда в сериализатор попадают данные изcore
, он уже готов выводить их в конкретный поток. Более того, из метода, собирающего данные, можно возвращать не сразу сами данные, а просто фьючер со ссылкой на них. При раскрытии этого фьючера внутри сериализатора автоматом произойдёт отложенный сбор данных.
MooNDeaR
19.01.2017 08:58На почему макрос DOCK_MODULE сделан вызываемым внутри параметров лямбда функции? Почему, чисто по причине удобства (не люблю писать скобочки), не сделать вот так:
Module(u8"Some module 2", DOCK_MODULE { //some code });
А сам макрос DOCK_MODULE не сделать таким:
DOCK_MODULE [](dock::_internal::Module* _dock_module)
Конечно никто мне не мешает самому написать подобную обертку, просто интересует причина подобного решения. Может быть это как-то используется, но не показано в статье?emdc
19.01.2017 12:25Пробовал так делать изначально, оставались вообще фигурные скобки и то, что внутри (
Module
тоже ведь макрос, поэтому я могу сразу в нем все скрыть):
Module(u8"Module name", { // ... });
Но в таком варианте почему-то ломались автоотступы в VS2015/VS2017, т.е. все блоки
Test
были без отступов вообще. Чтобы не раздражало пришлось перейти на менее читабельный и компактный вид.
dreamer-dead
19.01.2017 14:57+1Тут просто статистика по тесту типа прошел/не прошел и все?
По моему, этого обычно мало.
Зачастую тест содержит не один ассерт, а несколько. Как понять, кто из них сработал?
Как понять, какие значения ожидались, а какие там были на самом деле?
Как вывести дополнительную информацию о контексте ассерта?emdc
20.01.2017 11:42Так только старт же, по мере необходимости буду добавлять фичи :) Пока что мне нужно было что-то очень простое, что может выдавать результат в json, и эта потребность у меня закрылась. Может быть, кому-то еще пригодится такой простой движок, поэтому я и поделился им.
dreamer-dead
20.01.2017 14:26В итоге, это «что-то простое» превратится в большой кусок кода, который делает много вещей.
Иначе он будет мало полезен остальным.
Тогда встанет вопрос — пользоваться Вашим кодом, или протестированной либой с историей, тестами и поддержкой разных платформ, от крупного сообщества.
В любом случае, Вам — удачи, но я бы задумался над тем, чтобы попробовать заслать в апстрим то, что было нужно вместо велосипедостроения.
GTest, например, не так уж плоха.
vopl
20.01.2017 11:31static void isTrue(std::function<bool()> fcn);
чтобы все было пареллельно и перпендикулярно — надо позволить пользователю вычислять значение из тестируемого контекста, а не просто брать из ниоткуда?
templatestatic void isTrue(const T v, std::function<bool(const T&)> fcn = defaultIsTrueFunction());emdc
20.01.2017 11:39Для
std::function
контекст можно и с помощьюstd::bind
задать, но я из конкретно этого метода уже убрал std::function, оставил простоbool
.
EvilsInterrupt
20.01.2017 12:22Не пойму как сделать следующее:
1. Пометить тест как skiped?
2. Запустить только те что упали в прошлый раз?
3. Запустить тесты в рандомном порядке?
4. Запустить тесты несколько раз?
bfDeveloper
Начал использовать Catch только благодаря header-only. Ваша реализация тоже выглядит достойно, возможно попробую.