📜 ⬆️ ⬇️

PODAM Java Objects for Unit Testing



Good day!
When unit-testing, you often come across the need to fill in complex objects in order to return them from the side of the plugs or vice versa - to give them input to methods and tests. Some developers ignore Java get-set conventions, and even if there are getters and setters, filling the object with a fairly complex structure sometimes requires more code than the test itself. This is an anti-pattern Excessive Setup , 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 ideas of reasonable randomization of both the input data for the tests and the data returned by the stubs - I will show you with examples and search in the sources.
So, in order not to think for a long time, but not to engage in the world of animals, we will generate a country. Primitive, but sufficient for demonstration.

1. Model


The country will consist of the name, national currency and cities.
')
public class Country { private String name; private Currency currency; private List<City> cities; public Country() { setCities(new ArrayList<City>()); } //...  getters  setters } 

Cities will consist of the name, the number of inhabitants and the list of streets:

 public class City { private String name; private int population; private List<Street> streets; public City(String name) { this.name = name; } //...  getters  setters } 

Streets with titles

 public class Street { private String name; public Street(String name) { this.name = name; } //...  getters  setters } 

and currency (for example, with enum generation, they certainly do not work with currencies like that :))

 public enum Currency { RUB, EUR, USD; } 

So, the model is ready.

2. Helper class for beautiful output


Before generating the country, I allow myself to deviate a little to one side and create my RecursiveToStringStyle instead of what Apache (commons-lang3-3.4.jar) has in order to display the data received by PODAM using ReflectionToStringBuilder.
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. In myPojo is a full-fledged country - there are a lot of beeches, - so that 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 a negative population, and streets that are terrible 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


On the official website, they propose to implement the DataProviderStrategy interface, but there are 23 methods for each return type, for the size of the collection, etc. Perhaps, there are those who would like to and someone will even need to, but for the demonstration I wanted to find something simpler - I looked into the source code in search of what strategy was actually used in the previous paragraph - it turned out to be RandomDataProviderStrategy, but it is public final class. But inherited from AbstractRandomDataProviderStrategy - BINGO.
Getting to create your own strategy.

We want, for example:
1. One or two cities in our country - no more. And the result looks cumbersome :)
Overlapping
 @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.
Overlapping
 @Override public Integer getInteger(AttributeMetadata attributeMetadata) 


We will use the fact that AttributeMetadata contains two important methods:

 attributeMetadata.getAttributeName() /**   */ attributeMetadata.getPojoClass() /** ,     */ 


Carried on (the choice of cities is random, the choice of streets - googled a certain number of streets in Paris - sorry for formatting enum - but did not want to 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<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(); } } } 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, 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(); } } } 

Putting it all together and run 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 the full random:

 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. In fact, you can annotate @PodamStrategyValue to supply the fields themselves. To do this, you will need to implement the AttributeStrategy interface, thereby creating strategies for attributes. You can still have a lot of wonderful things, but this is for those who are interested in all this miracle - there are many tutorials on the official website .

2. Some may say that it is easier to plant specific values ​​into an object, no matter how difficult it is - I will not agree and I will not argue, so I immediately warn you about refusing to give comments on this topic - PODAM uses reflection, and this is access to fields with limited visibility and even constants (though I didn’t try through PODAM, but take reflection of 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 is partly possible. And in combination with Mockito, about which much has already been written, unit-testing (as well as blind pieces of the functional) becomes a sheer pleasure.

4. Of course, a link to a git turnip with what is described above.

Source: https://habr.com/ru/post/255425/


All Articles