
PODAM Java Objects for Unit Testing
- Tutorial

Good afternoon!
During unit testing, one often encounters the need to fill out complex objects in order to return them from the side of stubs, or vice versa - to give them input methods and tests. Some developers ignore Java get-set conventions, and even if there are getters and setters, filling an object with a rather complex structure sometimes requires more code than the test itself. This is an Excessive Setup anti-pattern , and I want to learn how to deal with it. In this article, I will tell you how to fill objects quickly and beautifully using the PODAM library, continuing the idea of reasonable randomization of both the input data for tests and the data returned by stubs - I'll show you examples, I will dig into the source.
So, in order not to think for a long time, but not to engage in the animal world, we will generate a country. Primitive, but sufficient for demonstration.
1. Model
The country will consist of denomination, national currency and cities.
public class Country {
private String name;
private Currency currency;
private List cities;
public Country() {
setCities(new ArrayList());
}
//... тут getters и setters
}
Cities will consist of the name, number of inhabitants and a list of streets:
public class City {
private String name;
private int population;
private List streets;
public City(String name) {
this.name = name;
}
//... тут getters и setters
}
Named Streets
public class Street {
private String name;
public Street(String name) {
this.name = name;
}
//... тут getters и setters
}
and currency (for example, generation with enum - certainly this does not work with currencies :))
public enum Currency {
RUB,
EUR,
USD;
}
So the model is ready.
2. Helper class for beautiful output
Before generating the country, I will allow myself to deviate a little to the side and create my RecursiveToStringStyle instead of what Apache has (commons-lang3-3.4.jar) so that with the help of ReflectionToStringBuilder I can output the data received using PODAM.
Class RecursiveToStringStyle
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. Generation
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()));
}
}
That's all. MyPojo is a full-fledged country - it turned out a lot of beeches - so those who wish can
expand the result
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"
]]
]]
]
In our country with the strange name “2n_BNdJOpE” and the national currency USD there are cities with no less strange names, sometimes with a negative population and streets that are scary to say out loud. This may be quite enough for many unit testing options, but I decided to see how deep the rabbit hole is.
4. Own generation strategy
The official site offers to implement the DataProviderStrategy interface, but there are 23 methods for each returned type, the size of the collection, etc. Perhaps there are people who want it and someone will even need to, but for the demonstration I wanted to find something simpler - I looked in the sources to find out which strategy was really used in the previous paragraph - it turned out to be RandomDataProviderStrategy, but it is public final class. But it inherits from AbstractRandomDataProviderStrategy - BINGO.
We begin to create our own strategy.
We want, for example:
1. One or two cities in our country - no more. And then the result looks bulky :) We
overlap
@Override
public int getNumberOfCollectionElements(Class type)
2. We want normal names of cities and streets - these will be two enum with static methods returning random elements to us.
Plus overlap
@Override
public String getStringValue(AttributeMetadata attributeMetadata)
3. We want the population - not negative, but, for example, from a million to 10 million.
Overlap
@Override
public Integer getInteger(AttributeMetadata attributeMetadata)
We will use the fact that AttributeMetadata contains two important methods:
attributeMetadata.getAttributeName() /** название поля */
attributeMetadata.getPojoClass() /** класс, в котором это поле */
It started (the choice of cities was random, the choice of streets - google a certain number of streets in Paris - sorry for formatting enum - but I didn’t want it vertically):
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 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 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();
}
}
}
Putting it all together and starting the generation:
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()));
}
}
The result is much nicer than using full randomization:
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. Conclusion
1. Actually, you can supply the fields themselves with @PodamStrategyValue annotations. To do this, you will need to implement the AttributeStrategy interface, thereby creating strategies for the attributes. You can still have a lot of wonderful things, but this is for those who are interested in this miracle - there are many tutorials on the official website .
2. Some may say that it’s easier to populate specific values into an object, no matter how difficult it is — I don’t agree and I won’t argue, so I immediately warn about refusing to comment on this topic - PODAM uses reflection, and this access to fields with limited visibility and even to constants (I didn’t try it through PODAM, but it takes reflection constants), and this opens up great opportunities not only for generating, but also for breaking, which is indispensable for automating negative testing.
3. The goal was to defeat the Excessive Setup anti-pattern. I think this was partially possible. And in combination with Mockito, about which much has already been written, unit testing (by the way, like stub pieces of functional) becomes a complete pleasure.
4. Of courselink to git turnip with what is described above.