Генератор тестовых данных для JVM совместимых языков
В этой статье речь пойдет о создании тестов в java приложениях, в первую очередь unit-тестов, а точнее, будем говорить о генерации тестовых данных. Вообще, я считаю проблему генерации тестовых данных в тестировании центровой. Во-первых, необходимо осознать какие же данные нужны для теста, во-вторых, их необходимо подготовить и сгенерировать. На проектах уровня hello world или при очень хорошей декомпозии проблема невелика, но на больших проектах с большими DTO, это мало того что сложно, так еще и занудно. В какой-то момент количество кода теста может многократно превышать количество тестируемого кода.
Предпосылки
На одном из проектов, где я работал, часть сущностей для тестов (DTO и пр.) генерировалась автоматически при помощи PodamFactory
(https://mtedone.github.io/podam/) и, в принципе, все было хорошо. Но, во-первых, сущности содержали адовое количетво полей. Во-вторых, хотелось бы, чтоб данные выглядели правдоподобно, например, если поле представляет собой название города, то там было какое-то название города, а не случайный набор символов. Во-вторых, на данные могут накладываться какие то ограничения, простым примером может служить ограничение на длинну строки поля. В-третьих, данные формально могут быть преставлены строкой, но при этом могут содержать в себе некоторую структуру, скажем, ИНН клиента в первых двух символах содержит код субьекта РФ и так далее.
Пожелания
В общем, захотелось писать много всяких хороших тестов, причем быстро, поэтому в конце концов вырисовались нижеследующие требования к генератору тестовых данных:
Генерирует правдоподобные данные, не режущие глаз, например, если поле является названием города, то там какое либо знакомое название;
Генерирует данные по простейшим ограничениям, как то длина строк или максимальные/минимальные значения численных данных или, скажем, берет рандомно какое-то значение из перечисления (
java.lang.Enum
);Генерирует данные в соответствии с некотрой внутренней структой, к примеру, ИНН клиента или, скажем, номер банковского счета, в котором часть символов обозначают код валюты;
Если мы имеем сложный DTO, то иметь возможность большую часть полей сгенерировать по определенным общим правилам, а часть полей в данном месте теста описать более подробно, к примеру, 50 полей DTO объекта сгенерированы по общим условиям, а номер телефона задать конкретный, желаемый именно в данном месте теста;
Хотелось бы иметь максимально простой способ описания наших требований к тестовым данным, в идеале какой-то декларативный способ;
Хотелось бы из коробки иметь возможность генерировать экземпляры самых распространенных классов, во всяком случае те, что идут с JRE/JDK.
Вперед к светлому будущему…
Погружение в недра, упомянутой выше, PodamFactory
показало, что гибкости, и главное, простоты не получится и, недолго думая, решено было написать свой велосипед, который бы удовлетворял требованиям, изложенным выше. Забегая вперед, скажу, что текущая версия велосипеда третья, первая версия, как тому и положено по законам жанра, отправилась в мусорное ведро, а вторая получилась как бы уже не первая, но и не третья.
Итак, прошу любить и жаловать проект genthz.org (genthz on github).
Как это работает
В java создание объектов можно разложить на два этапа: первый — это, собственно, создание экземпляра класса (выделение памяти и начальная инициализация) при помощи конструктора или статического метода-фабрики (под капотом, опять же конструктор) и второй — заполнение полей объекта, которые не были (а может и были) проинициализированы на первом этапе. Весь этот процесс очевидно рекурсивный, потому что каждое поле объекта это тоже объект, который надо создать и заполнить. Также, можно выделить отдельный класс объектов, для которых второй этап — заполнения, необходимо пропустить. К таким объектам можно отнести все примитивы java, а так же всякие java.lang.String
, java.math.BigInteger
и прочие, которые вы можете придумать сами.
Для каждого этапа (org.genthz.context.Stage
) решено было сделать отдельный интерфейс, реализации которого обеспечили бы выполнение этого этапа. Для первой фазы CREATING (создание объекта) используются org.genthz.function.InstanceBuilder
, для второй фазы FILLING (заполнение) используется org.genthz.function.Filler
.
Очевидно, что у нас есть масса стандартных классов, которые идут с JRE/JDK и для них хотелось бы иметь instance builder«ы и filer«ы по умолчанию. И они есть (пока только для java 1.8), параметры для многих страндартных классов можно найти в org.genthz.Defaults. Система по умолчанию умеет генерировать примитивы, строки, коллекции, стримы и пр.
Вообще, в пакете верхнего уровня (org.genthz
) вы найдете в основном интерфейсы, для них существует реализация по умолчанию в пакете (org.genthz.dasha
).
Life samples
Итак, давайте попробуем сгенерировать что-то для нашего проекта при помощи библиотеки. Как уже было сказано выше, библиотека имеет много генераторов по умолчанию и мы можем начать её использовать сразу из коробки, для этого импортируем ее в свой проект из репозитория maven:
org.genthz
genthz-core
3.1.3
Далее, получает объект класса ObjectFactory
и генерируем объекты.
Пример генерации из коробки
Пример генерации строки:
ObjectFactory objectFactory = new DashaObjectFactory();
String value = objectFactory.get(String.class);
Работа с объектами и собственными алгоритмами создания (instanseBuilder) и заполнения (filler) объектов
Можно работать со сложными объектами:
public class Person {
private Long id;
private String name;
private String lastName;
private LocalDate birthDate;
public Person() {
}
}
Пример генерации объекта Person
ObjectFactory objectFactory = new DashaObjectFactory();
Person person = objectFactory.get(Person.class);
Что делать, если хочется поменять алгоритм создания (instanceBuilder) объекта? Это легко сделать:
Person value = new DashaDsl() {
{
// Укажем, что хотим задать правила для генерации объектов класса Person.
strict(Person.class)
// Вызовем метод simple, чтобы указать, что после создания объекта пропустить фазу заполнения его полей.
.simple(ctx -> {
Person person = new Person();
person.setId(1L);
person.setName("Oliver");
person.setLastName("Brown");
person.setBirthday(LocalDate.now());
return person;
});
}
}
// Метод def() вызываем, что бы подключить генерацию объектов по умолчанию.
.def()
.objectFactory()
.get(Person.class);
Можно так же изменить алгоритм (filler) заполнения объекта:
Person value = new DashaDsl() {
{
// Укажем, что хотим задать правила для генерации объектов класса Person.
strict(Person.class)
// Укажем что хотим использовать свой алгоритм заполнения объекта (filler).
.filler(ctx -> {
// Get instance created by default instance builder.
Person person = ctx.instance();
person.setId(10L);
person.setName("Oliver");
person.setLastName("Brown");
person.setBirthday(LocalDate.now());
}));
}
}
// Метод def() вызываем, что бы подключить генерацию объектов по умолчанию.
.def()
.objectFactory()
.get(Person.class);
Можно задать одновременно и свой instanceBuilder и filler:
Person value = new DashaDsl() {
{
// Укажем, что хотим задать правила для генерации объектов класса Person.
strict(Person.class)
// Укажем алгоритм для генерации объекта типа Person.
.instanceBuilder(ctx -> new Person)
// Укажем алгоритм заполнения полей объекта.
.filler(ctx -> {
// Get instance created by default instance builder.
Person person = ctx.instance();
person.setName("Oliver");
person.setLastName("Brown");
person.setBirthday(new Date());
}));
}
}
// Метод def() вызываем, что бы подключить генерацию объектов по умолчанию.
.def()
.objectFactory()
Рекурсия
Что делать если в структуре объекта есть рекурсия?
public class Recursion {
private Recursion recursion;
}
По идее, должно случиться что-то типа java.lang.StackOverflowError, однако, в DashaDsl
устанавливается специальный filler, который прерывает цепочку генерации на глубине Defaults#defaultMaxGenerationDepth()
. В принципе, этот параметр можно менять по мере надобности:
Recursion value = new DashaDsl()
// Установить параметры генераторов по умолчанию.
.defaults(new DashaDefaults(){
// Изменить глубину рекурсии на 10.
@Override
public Function defaultMaxGenerationDepth() {
return ctx -> 10L;
}
})
// Метод def() вызываем, что бы подключить генерацию объектов по умолчанию.
.def()
.objectFactory()
.get(Recursion.class);
Использование путей (path)
Иногда при генерации объекта необходимо задать кастомное правило только для одного из его полей, это можно сделать как при помощи instanceBuilder:
Person value = new DashaDsl() {
{
// Укажем, что хотим задать правила для генерации объектов класса Person.
strict(Person.class)
// Укажем, что надо сгенерировать только поле «name».
.path("name")
// Указываем, используя simple, что нужно создать объект, но не заполнять его поля.
.simple(ctx -> "Alex");
}
}
// Метод def() вызываем, что бы подключить генерацию объектов по умолчанию.
.def()
.objectFactory()
.get(Person.class);
так и при помощи filler:
Person value = new DashaDsl() {
{
// Укажем, что хотим задать правила для генерации объектов класса Person.
strict(Person.class)
// Укажем, что надо сгенерировать только поле «name
.path("name")
// Задаем алгоритм его заполнения.
.filler(ctx -> ctx.set("Alex"));
}
}
// Метод def() вызываем, что бы подключить генерацию объектов по умолчанию.
.def()
.objectFactory()
.get(Person.class);
Можно указать, что мы хотим генерировать кастомные значения для полей с именем «name» и типом java.lang.String
любых объектов:
Person value = new DashaDsl() {
{
// Укажем, что надо сгенерировать только поле «name».
path("name")
// Тип поля должен быть String.
.strict(String.class )
// Зададим instanceBuiler и укажем, что после создания заполнять объект не нужно.
.simple(ctx -> "Alex");
}
}
// Метод def() вызываем, что бы подключить генерацию объектов по умолчанию.
.def()
.objectFactory()
.get(Person.class);
Generics
Как работать если в описании класса есть generics? Нужно просто указать их при вызове метода ObjectFactory.get()
. Если в качестве generics используются обычные классы, то просто подставляем их:
Map map = objectFactory.get(
Map.class,
String.class,
Person.class
);
А что делать, если у нас есть generics of generics of generics?! Нет никаких проблем, просто нужно передать объекты типа java.lang.reflect.ParameterizedType
. Но это интерфейс и что бы его получить можно воспользоваться, к примеру, утилитным классом org.apache.commons.lang3.reflect.TypeUtils
библиотеки commons-lang3
или любой другой по работе с reflection.
public class FuriousGenerics {
private A field0;
private Map> fiedl1;
private Deque
FuriousGenerics
Размеры генерируемых коллекций
По умолчанию размер генерируемых коллекций возвращается методом Defaults.defaultCollectionSize()
. Поэтому, если вы хотите изменить размеры всех коллекций сразу, то можете сделать это так:
ObjectFactory objectFactory = new DashaDsl() {
{
defaults(new DashaDefaults(){
@Override
public int defaultCollectionSize() {
// Размер коллекций по умолчанию равен 50.
return 50;
}
});
}
}
.def()
.objectFactory();
Если для какого-то типа коллекций нужно указать свой размер, то это можно сделать так:
ObjectFactory objectFactory = new DashaDsl() {
{
unstrict(Deque.class)
.filler(CollectionFillers.size(10));
}
}
.def()
.objectFactory();
В примере выше мы использовали функцию unstrict
для указания типа коллекции что бы казать, что размер задается для коллекций типа Deque
, а также всех его потомков и параметризаций, т.е. Для ArrayDeque
и, например, для Deque
.
Другими словами, если вы хотите задать размер только для Deque
, то стоит поступить так:
ObjectFactory objectFactory = new DashaDsl() {
{
strict(Deque.class, String.class)
.filler(CollectionFillers.size(10));
}
}
.def()
.objectFactory();
Вместо заключения
Эта статья — небольшая презентация проекта. Более подробную информацию можно получить в описании проекта на сайте https://genthz.org, которое я скоро также обновлю. В других статьях я продолжу обсуждать тему генерации данных и развития этого проекта.
Я буду рад, если моя работа поможет кому-то упростить работу с тестами в своих проектах. И буду особенно благодарен, если кто-то найдя ошибки в работе генератора, сообщит мне о них. Кроме того, если у вас есть идеи относительно улучшения юзабилити генератора, пишите мне о них или заводите задачи на сайте genthz на github .