Pull to refresh

Генератор тестовых данных для C++

Reading time4 min
Views10K

image


При unit-тестированиии кода рано или поздно встает вопрос тестовых данных. И если в одном случае достаточно просто несколько жестко зашитых переменных, то в других случаях необходимы сколько-нибудь большие и случайные данные. В управляемом мире нет проблем с генерацией пользовательских типов (взять тот же Autofixture), но мир C++ зачастую вызывает боль и страдание (поправьте меня, если это не так). Не так давно я познакомился с замечательной библиотекой boost::di и под ее влиянием у меня начала созревать идея библиотеки, которая позволила бы C++ программистам генерировать пользовательские типы данных, забитых случайными значаниями, и это не потребовало бы предварительного их описания. Получилось что-то вроде:


struct dummy_member{
    float a;
    int b;
};
struct dummy{
    explicit dummy(dummy_member val, std::string c) : val_(val), c_(c) {}
private:
    dummy_member val_;
    std::string c_;
};
int main(int argc, char* argv){
    auto d = datagen::random<dummy>();
    return 0;
}

Ссылка на код. Библиотека header-only,C++14. Всех интересующихся прошу под кат.


Основные возможности библиотеки


  • Генерация встроенных типов
  • Генерация пользовательских типов(==не встроенных)
  • Ограничители на множество генерируемых значений

Генерация встроенных типов


Естественно поддерживается генерация встроенных типов (char,wchar_t и прочее). При этом целочисленные типы генерируются просто как набор битов, а float и double — как сумма случайного целочисленного (int32_t и int64_t соответственно) и случайного значений в интервале от -1 до 1. Для генерации bool значения используется сравнение двух случайных целых.


std::cout << "The answer to the question of everything is:" << datagen::random<int>() << std::endl;

Генерация пользовательских типов.


Для генерации пользовательских типов за основу была взята та же идея, что и в boost::di (спасибо ее автору), а именно возможность написания универсального типа any_type, неявно конвертируемого в любой другой(за редким исключения). Добавив НЕМНОГО шаблонов, получилась штука, генерирующая пользовательские типы, используя следующие средства:


  1. Определенный пользователем алгоритм генерации.
  2. Генерация на основе публичного конструктора с максимальным количеством параметров. Все как в примере в начале статьи (struct dymmy).
  3. Генерация на основе {}-инициализации. Все так же, как в первом примере (struct dummy_member).

Для генерации объектов на основе определенной пользователем процедуры необходимо частично или полностью специализировать шаблон


template<> struct datagen::value_generation_algorithm<TType> { 
    TType get_random(random_source_base&); 
};

Это добавляет возможность выносить некоторые параметры генерации типов в члены этого класса, что в свою очередь позволяет влиять на генерацию типов. Например, алгоритм генерации строк из std выглядит так:


namespace datagen{
    template <class CharType, class Traits, class Allocator>
    struct value_generation_algorithm<std::basic_string<CharType, Traits, Allocator>>{
        using string_t = std::basic_string<CharType, Traits, Allocator>;
        string_t get_random(random_source_base& r_source){...};
        size_t min_size{0};
        size_t max_size{30};
        std::basic_string<CharType> alphabet{"abcd...6789"};
    };
}

Ограничители на множество генерируемых значений


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


std::cout << "The answer to the question of everything is:" << random<int>(between(42,42)) << std::endl;

Есть 2 вида ограничителей:


  1. Ограничители на алгоритм генерации. С их помощью можно менять значения параметров в классе value_generation_algorithm<T>.
  2. Ограничители (скорее корректоры) на уже сгенерированное значение.

При это они могут быть использованы 2 способами:


  1. Передача их в качестве параметра в функцию random, как в примере выше. В этом случае они будут применены только к текущему алгоритму/значению.
  2. Создание на их основе scoped_limit и применение его к набору типов. Тогда ограничитель применяется для всех указанных типов для всей глубины генерируемого дерева типов на протяжении жизни scoped_limit-а.

Для создания пользовательских ограничителей необходимо объявить структуру/класс ограничителя и реализовать одну или обе функции:


struct dummy_algorithm_limit{};
struct dummy_value_limit{};

namespace datagen{
    namespace limits    {
        void adjust_algorithm(random_source_base&, dummy_algorithm_limit const& l, value_generation_algorithm<dummy>& a){
        // здесь можно подправить параметры генерации dummy
        }

        void adjust_value(random_source_base&, dummy_value_limit const& l, dummy& a){
        //здесь можно подправить сгенерированное значение dummy
        }
    }
}

Ограничения применяются в следующем порядке:


  1. scoped_limit для алгоритмов
  2. параметрические ограничители на алгоритм
    здесь идет генерация дерева объектов
  3. scoped_limit для значений
  4. параметрические ограничители на значения

Общая информация


Источником энтропии в библиотеке является класс random_source_impl, использующий <random>. Но есть возможность переопределить это, предоставив на этапе компиляции специализацию структуры random_source_instance<int>.
На сегодня реализована (собственно то, что необходимо мне в работе) генерация следующих контейнеров из stl:


  • std::array
  • std::map
  • std::set
  • std::string
  • std::vector

пары типов из boost:


  • boost::asio::ip::address (v4,v6)
  • boost::optional
  • boost::posix_time::ptime
  • boost::posix_time::time_duration

Ограничители для них:


  • between для встроенных типов и не только
  • greater_than,less_than, odd, even
  • container_size::between, container_size::less_than и т.д.
  • alphabet::consists_of, alphabet::does_not_contain

Тестировалось на компиляторе msvc-14.0, требует c++14. К сожалению, gcc ведет себя немного по-другому, в следствие чего код библиотеки на собрался под mingw (gcc-6.3.0), но думаю те, кто имеет с ним постоянный контакт, смогут быстро это поправить.
Библиотека лежит в открытом доступе. Идеи и реализации новых типов приветствуются.

Only registered users can participate in poll. Log in, please.
А как вы генерируете случайные данные для C++ unit-тестов?
48.08% никак-данные зашиты в код тестов25
5.77% всяческие фабрики объектов, привязанные к тестовому проекту3
13.46% самописная библиотека-генератор7
1.92% сторонние библиотеки (просьба указать в комментариях)1
42.31% я не пишу тестов22
3.85% другое (в комментариях)2
52 users voted. 25 users abstained.
Tags:
Hubs:
Total votes 13: ↑10 and ↓3+7
Comments16

Articles