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.

Also popular now: