Добрый день!
При unit-тестировании часто сталкиваешься с необходимостью заполнять сложные объекты, чтобы возвращать их со стороны заглушек или наоборот — давать их на вход методам и тестам. Некоторые разработчики игнорируют get-set конвенции Java, а даже если геттеры и сеттеры есть, то заполнение объекта достаточно сложной структуры порой требует больше кода, чем сам тест. Это анти-паттерн Excessive Setup, и хочется научиться с ним бороться. В этой статье я расскажу, как с помощью библиотеки PODAM заполнять объекты быстро и красиво, продолжая идеи разумной рандомизации как входных данных для тестов, так и данных, возвращаемых заглушками — покажу на примерах, пороюсь в исходниках.
Итак, чтобы долго не думать, но и не заниматься миром животных, сгенерим страну. Примитивную, но достаточную для демонстрации.
1. Модель
Страна будет состоять из наименования, национальной валюты и городов.
public class Country {
private String name;
private Currency currency;
private List<City> cities;
public Country() {
setCities(new ArrayList<City>());
}
//... тут getters и setters
}
Города будут состоять из наименования, количества жителей и списка улиц:
public class City {
private String name;
private int population;
private List<Street> streets;
public City(String name) {
this.name = name;
}
//... тут getters и setters
}
Улицы с наименованиями
public class Street {
private String name;
public Street(String name) {
this.name = name;
}
//... тут getters и setters
}
и валюта (для примера генерации с enum-ами — безусловно так не работают с валютами :) )
public enum Currency {
RUB,
EUR,
USD;
}
Итак, модель готова.
2. Вспомогательный класс для красивого вывода результатов
Прежде чем генерить страну я позволю себе отклониться немного в сторону и создам свой RecursiveToStringStyle взамен тому, что есть у Apache (commons-lang3-3.4.jar), чтобы с помощью ReflectionToStringBuilder выводить получаемые с помощью PODAM данные.
public class RecursiveToStringStyle extends ToStringStyle {
private static final long serialVersionUID = 1L;
private int offset;
public RecursiveToStringStyle() {
this(0);
}
private RecursiveToStringStyle(int offset) {
setUseShortClassName(true);
setUseFieldNames(true);
setUseIdentityHashCode(false);
this.offset = offset;
String off = "";
for (int i = 0; i < offset; i++)
off += "t";
this.setContentStart("[");
this.setFieldSeparator(SystemUtils.LINE_SEPARATOR + off + " ");
this.setFieldSeparatorAtStart(true);
this.setContentEnd(SystemUtils.LINE_SEPARATOR + off + "]");
}
protected void appendDetail(StringBuffer buffer, String fieldName,
Collection<?> col) {
buffer.append('[');
for (Object obj : col) {
buffer.append(ReflectionToStringBuilder.toString(obj,
new RecursiveToStringStyle(offset + 1)));
buffer.append(',');
}
if (buffer.charAt(buffer.length() - 1) == ',')
buffer.setCharAt(buffer.length() - 1, ']');
}
protected void appendDetail(StringBuffer buffer, String fieldName,
Object value) {
if (value instanceof String) {
buffer.append(""" + value.toString() + """);
} else if (value instanceof BigDecimal) {
buffer.append(value.getClass().getSimpleName() + "["
+ value.toString() + "]");
} else if (value instanceof BigInteger) {
buffer.append(value.getClass().getSimpleName() + "["
+ value.toString() + "]");
} else if (!value.getClass().getName().startsWith("java.lang.")) {
try {
buffer.append(ReflectionToStringBuilder.toString(value,
new RecursiveToStringStyle(offset + 1)));
} catch (Throwable t) {
}
} else {
super.appendDetail(buffer, fieldName, value);
}
}
}
3. Генерация
public class CountryCreatorSimple {
public static void main(String[] args) {
/** Создаём фабрику */
PodamFactory factory = new PodamFactoryImpl();
/** Генерим страну */
Country myPojo = factory.manufacturePojo(Country.class);
/** "Печатаем" страну */
System.out.println(ReflectionToStringBuilder.toString(myPojo,new RecursiveToStringStyle()));
}
}
Вот собственно и всё. В myPojo полноценная страна — получилось много буков, — так что желающие могут
Country[
name="2n_BNdJOpE"
currency=Currency[
name="USD"
ordinal=2
]
cities=[City[
name="7_BmoRTDab"
population=-1863637717
streets=[Street[
name="XV_q7SPbvk"
],Street[
name="GkNGKj6B9J"
],Street[
name="y9GNakRAsW"
],Street[
name="Mwo09nQx0R"
],Street[
name="n4_EDMGNUR"
]]
],City[
name="1sifHwujvo"
population=1832262487
streets=[Street[
name="xpZiJH2sce"
],Street[
name="ns8DRJDi4e"
],Street[
name="7Ijv_UVZrF"
],Street[
name="CYruDEhe2M"
],Street[
name="4HFzN0v5mc"
]]
],City[
name="qJlUWEPoxp"
population=1979728140
streets=[Street[
name="_LbqmCPgWC"
],Street[
name="yS6jX8vRqI"
],Street[
name="yFysWkntdh"
],Street[
name="RvP93uJphY"
],Street[
name="WjARSGWfxB"
]]
],City[
name="W1J9mWpEFH"
population=493149274
streets=[Street[
name="8bFRRbPmqO"
],Street[
name="ORJ4rP1i41"
],Street[
name="qD9XU0I0K2"
],Street[
name="I75Wt5cK9v"
],Street[
name="viT8t5FkPq"
]]
],City[
name="33cPIh6go9"
population=693664641
streets=[Street[
name="kvPtj1GIL4"
],Street[
name="aVv1taDA0j"
],Street[
name="iQ6ZriwuZK"
],Street[
name="fcf6JICEQ9"
],Street[
name="1Pbdnc_7R6"
]]
]]
]
В нашей стране со странным названием «2n_BNdJOpE» и национальной валютой USD есть города с не менее странными названиями, порой отрицательной численностью населения и с улицами, которые страшно произносить вслух. Этого может быть вполне достаточно для многих вариантов unit-тестирования, но я решил посмотреть насколько глубока кроличья нора.
4. Собственная стратегия генерации
На официальном сайте предлагают имплементировать интерфейс DataProviderStrategy, но там оказывается 23 метода на каждый возвращаемый тип, на размер коллекции и т.д. Возможно, есть желающие и кому-то даже нужно будет, но для демонстрации хотелось найти что-то попроще — заглянул в исходники в поисках того, какая стратегия реально использовалась в предыдущем пункте — оказалось RandomDataProviderStrategy, но она public final class. Зато наследуется от AbstractRandomDataProviderStrategy — BINGO.
Приступаем к созданию собственной стратегии.
Хотим, например:
1. Один или два города в нашей стране — не больше. А то результат смотрится громоздко :)
Перекрываем
@Override
public int getNumberOfCollectionElements(Class<?> type)
2. Хотим нормальные названия городов и улиц — это будут два enum-а со статическими методами возвращающими нам случайные элементы.
Плюс перекрываем
@Override
public String getStringValue(AttributeMetadata attributeMetadata)
3. Хотим население — не отрицательное, а, например от миллиона до 10 миллионов.
Перекрываем
@Override
public Integer getInteger(AttributeMetadata attributeMetadata)
Будем пользоваться тем, что AttributeMetadata содержит в себе два важных метода:
attributeMetadata.getAttributeName() /** название поля */
attributeMetadata.getPojoClass() /** класс, в котором это поле */
Понеслась (выбор городов случаен, выбор улиц — нагуглил некоторое количество улиц в Париже — сорри за форматирование enum — но не хотелось вертикально):
public class CountryDataProviderStrategy extends
AbstractRandomDataProviderStrategy {
private static final Random random = new Random(System.currentTimeMillis());
public CountryDataProviderStrategy() {
super();
}
@Override
public String getStringValue(AttributeMetadata attributeMetadata) {
/**
* Если поле name, то в зависимости от класса либо генерим улицу, либо
* город, либо страну
*/
if ("name".equals(attributeMetadata.getAttributeName())) {
if (Street.class.equals(attributeMetadata.getPojoClass())) {
return Streets.randomStreet();
} else if (City.class.equals(attributeMetadata.getPojoClass())) {
return Cities.randomCity();
} else if (Country.class.equals(attributeMetadata.getPojoClass())) {
return "Podam States of Mockitia";
}
}
return super.getStringValue(attributeMetadata);
};
@Override
public int getNumberOfCollectionElements(Class<?> type) {
/**
* Если список городов, то вернём или 1 или 2. Если список улиц, то
* вернём от 1 до 10
*/
if (City.class.getName().equals(type.getName())) {
return 1 + random.nextInt(2);
} else if (Street.class.getName().equals(type.getName())) {
return 1 + random.nextInt(10);
}
return super.getNumberOfCollectionElements(type);
};
@Override
public Integer getInteger(AttributeMetadata attributeMetadata) {
/** Ну и вернём разумное население */
if (City.class.equals(attributeMetadata.getPojoClass())) {
if ("population".equals(attributeMetadata.getAttributeName())) {
return 1_000_000 + random.nextInt(9_000_000);
}
}
return super.getInteger(attributeMetadata);
}
private enum Cities {
MOSCOW, SAINT_PETERSBURG, LONDON, NEW_YORK, SHANGHAI, KARACHI, BEIJING, DELHI, PARIS, NAIROBI;
private static final List<Cities> values = Collections.unmodifiableList(Arrays.asList(values()));
private static final int size = values.size();
private static final Random random = new Random();
public static String randomCity() {
return values.get(random.nextInt(size)).toString();
}
}
private enum Streets {
RUE_ABEL, RUE_AMPERE, AVENUE_PAUL_APPELL, BOULEVARD_ARAGO, JARDINS_ARAGO, SQUARE_ARAGO, RUE_ANTOINE_ARNAULD, SQUARE_ANTOINE_ARNAULD, RUE_BERNOULLI, RUE_BEZOUT, RUE_BIOT, RUE_BORDA, SQUARE_BOREL, RUE_CHARLES_BOSSUT, RUE_DE_BROGLIE, RUE_BUFFON, AVENUE_CARNOT, BOULEVARD_CARNOT, VILLA_SADI_CARNOT, RUE_CASSINI, RUE_CAUCHY, RUE_MICHEL_CHASLES, RUE_NICOLAS_CHUQUET, RUE_CLAIRAUT, RUE_CLAPEYRON, RUE_CONDORCET, RUE_CORIOLIS, RUE_COURNOT, RUE_GASTON_DARBOUX, RUE_DELAMBRE, SQUARE_DELAMBRE, RUE_DEPARCIEUX, RUE_DE_PRONY, RUE_DESARGUES, RUE_DESCARTES, RUE_ESCLANGON, RUE_EULER;
private static final List<Streets> values = Collections.unmodifiableList(Arrays.asList(values()));
private static final int size = values.size();
private static final Random random = new Random();
public static String randomStreet() {
return values.get(random.nextInt(size)).toString();
}
}
}
Собираем всё воедино и запускаем генерацию:
public class CountryCreatotWithStrategy {
public static void main(String[] args) {
/** Создаём нашу стратегию генерации */
DataProviderStrategy strategy = new CountryDataProviderStrategy();
/** Создаём фабрику на основании этой стратегии */
PodamFactory factory = new PodamFactoryImpl(strategy);
/** Генерим страну */
Country myPojo = factory.manufacturePojo(Country.class);
/** Печатаем страну */
System.out.println(ReflectionToStringBuilder.toString(myPojo,new RecursiveToStringStyle()));
}
}
Результат намного приятнее, чем с использованием полного рандома:
Country[
name="Podam States of Mockitia"
currency=Currency[
name="RUB"
ordinal=0
]
cities=[City[
name="NAIROBI"
population=9563403
streets=[Street[
name="RUE_BORDA"
],Street[
name="RUE_DE_PRONY"
],Street[
name="SQUARE_ANTOINE_ARNAULD"
],Street[
name="RUE_CASSINI"
],Street[
name="RUE_DE_PRONY"
]]
],City[
name="PARIS"
population=7602177
streets=[Street[
name="RUE_DESCARTES"
],Street[
name="RUE_CHARLES_BOSSUT"
]]
]]
]
5. Заключение
1. На самом деле можно аннотациями @PodamStrategyValue снабжать сами поля. Для этого надо будет имплементировать интерфейс AttributeStrategy, создав тем самым стратегии для атрибутов. Можно ещё много всего замечательного, но это уже для тех, кого всё это чудо заинтересовало — на официальном сайте есть много туториалов.
2. Некоторые могут сказать, что проще засетить конкретные значения в объект, как бы это сложно ни было — я не соглашусь и спорить не буду, так что сразу предупреждаю об отказе от дачи комментариев на эту тему — PODAM использует reflection, а это доступ к полям с ограниченной видимостью и даже к константам (через PODAM правда не пробовал, но reflection константы берёт), а это открывает большие возможности не только для погенерить, но и для поломать, что незаменимо для автоматизации негативного тестирования.
3. Цель была победить Excessive Setup анти-паттерн. Думаю, отчасти это удалось. А в сочетании с Mockito, о котором уже написано много, unit-тестирование (впрочем, как и заглушечные куски функционального) становится сплошным удовольствием.
4. Конечно же, ссылка на git-овую репу с тем, что описано выше.
Автор: FranciscoSuarez