com.codebox.bean.JavaBeanTesterWorker Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of javabean-tester Show documentation
Show all versions of javabean-tester Show documentation
Javabean Tester Simplifies POJO Testing and greatly simplifies Lombok Bean Testing!
The newest version!
/*
* JavaBean Tester (https://github.com/hazendaz/javabean-tester)
*
* Copyright 2012-2023 Hazendaz.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of The Apache Software License,
* Version 2.0 which accompanies this distribution, and is available at
* http://www.apache.org/licenses/LICENSE-2.0.txt
*
* Contributors:
* CodeBox (Rob Dawson).
* Hazendaz (Jeremy Landis).
*/
package com.codebox.bean;
import com.codebox.enums.CheckClear;
import com.codebox.enums.CheckConstructor;
import com.codebox.enums.CheckEquals;
import com.codebox.enums.CheckSerialize;
import com.codebox.enums.LoadData;
import com.codebox.enums.LoadType;
import com.codebox.enums.SkipStrictSerialize;
import com.codebox.instance.ClassInstance;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import lombok.Data;
import net.sf.cglib.beans.BeanCopier;
import nl.jqno.equalsverifier.EqualsVerifier;
import org.junit.jupiter.api.Assertions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The Class JavaBeanTesterWorker.
*
* @param
* the generic type
* @param
* the element type
*/
@Data
class JavaBeanTesterWorker {
/** The Constant LOGGER. */
private static final Logger LOGGER = LoggerFactory.getLogger(JavaBeanTesterWorker.class);
/** The check clear. */
private CheckClear checkClear;
/** The check constructor. */
private CheckConstructor checkConstructor;
/** The check equals. */
private CheckEquals checkEquals;
/** The check serializable. */
private CheckSerialize checkSerializable;
/** The load data. */
private LoadData loadData;
/** The clazz. */
private final Class clazz;
/** The extension. */
private Class extension;
/** The skip strict serialize. */
private SkipStrictSerialize skipStrictSerializable;
/** The skip these. */
private Set skipThese = new HashSet<>();
/**
* Instantiates a new java bean tester worker.
*
* @param newClazz
* the clazz
*/
JavaBeanTesterWorker(final Class newClazz) {
this.clazz = newClazz;
}
/**
* Instantiates a new java bean tester worker.
*
* @param newClazz
* the clazz
* @param newExtension
* the extension
*/
JavaBeanTesterWorker(final Class newClazz, final Class newExtension) {
this.clazz = newClazz;
this.extension = newExtension;
}
/**
* Tests the load methods of the specified class.
*
* @param
* the type parameter associated with the class under test.
* @param clazz
* the class under test.
* @param instance
* the instance of class under test.
* @param loadData
* load recursively all underlying data objects.
* @param skipThese
* the names of any properties that should not be tested.
*
* @return the java bean tester worker
*/
public static JavaBeanTesterWorker load(final Class clazz, final L instance,
final LoadData loadData, final String... skipThese) {
final JavaBeanTesterWorker worker = new JavaBeanTesterWorker<>(clazz);
worker.setLoadData(loadData);
if (skipThese != null) {
worker.setSkipThese(new HashSet<>(Arrays.asList(skipThese)));
}
worker.getterSetterTests(instance);
return worker;
}
/**
* Tests the clear, get, set, equals, hashCode, toString, serializable, and constructor(s) methods of the specified
* class.
*/
public void test() {
// Test Getter/Setter
this.getterSetterTests(new ClassInstance().newInstance(this.clazz));
// Test Clear
if (this.checkClear != CheckClear.OFF) {
this.clearTest();
}
// Test constructor
if (this.checkConstructor != CheckConstructor.OFF) {
this.constructorsTest();
}
// Test Serializable (internally uses on/off/strict checks)
this.checkSerializableTest();
// Test Equals
if (this.checkEquals == CheckEquals.ON) {
this.equalsHashCodeToStringSymmetricTest();
}
}
/**
* Getter Setter Tests.
*
* @param instance
* the instance of class under test.
*
* @return the ter setter tests
*/
void getterSetterTests(final T instance) {
final PropertyDescriptor[] props = this.getProps(this.clazz);
for (final PropertyDescriptor prop : props) {
Method getter = prop.getReadMethod();
final Method setter = prop.getWriteMethod();
// Java Metro Bug Patch (Boolean Wrapper usage of 'is' possible
if (getter == null && setter != null) {
final String isBooleanWrapper = "is" + setter.getName().substring(3);
try {
getter = this.clazz.getMethod(isBooleanWrapper);
} catch (NoSuchMethodException | SecurityException e) {
// Do nothing
}
}
if (getter != null && setter != null) {
// We have both a get and set method for this property
final Class> returnType = getter.getReturnType();
final Class>[] params = setter.getParameterTypes();
if (params.length == 1 && params[0] == returnType) {
// The set method has 1 argument, which is of the same type as the return type of the get method, so
// we can test this property
try {
// Build a value of the correct type to be passed to the set method
final Object value = this.buildValue(returnType, LoadType.STANDARD_DATA);
// Build an instance of the bean that we are testing (each property test gets a new instance)
final T bean = new ClassInstance().newInstance(this.clazz);
// Call the set method, then check the same value comes back out of the get method
setter.invoke(bean, value);
// Use data set on instance
setter.invoke(instance, value);
final Object expectedValue = value;
Object actualValue = getter.invoke(bean);
// java.util.Date normalization patch
//
// Date is zero based so it adds 1 through normalization. Since we always pass '1' here, it is
// the same as stating February. Thus we roll over the month quite often into March towards
// end of the month resulting in '1' != '2' situation. The reason we pass '1' is that we are
// testing the content of the object and have no idea it is a date to start with. It is simply
// that it sees getters/setters and tries to load them appropriately. The underlying problem
// with that is that the Date object performs normalization to avoid dates like 2-30 that do
// not exist and is not a typical getter/setter use-case. It is also deprecated but we don't
// want to simply skip all deprecated items as we intend to test as much as possible.
//
if (this.clazz == Date.class && prop.getName().equals("month")
&& expectedValue.equals(Integer.valueOf("1"))
&& actualValue.equals(Integer.valueOf("2"))) {
actualValue = Integer.valueOf("1");
}
Assertions.assertEquals(expectedValue, actualValue,
String.format("Failed while testing property '%s' of class '%s'", prop.getName(),
this.clazz.getName()));
} catch (final IllegalAccessException | IllegalArgumentException | InvocationTargetException
| SecurityException e) {
Assertions.fail(String.format(
"An exception was thrown while testing class '%s' with the property (getter/setter) '%s': '%s'",
this.clazz.getName(), prop.getName(), e.toString()));
}
}
}
}
}
/**
* Clear test.
*/
void clearTest() {
final Method[] methods = this.clazz.getDeclaredMethods();
for (final Method method : methods) {
if (method.getName().equals("clear")) {
final T newClass = new ClassInstance().newInstance(this.clazz);
final T expectedClass = new ClassInstance().newInstance(this.clazz);
try {
// Perform any Post Construction on object without parameters
List annotations = null;
for (final Method mt : methods) {
annotations = Arrays.asList(mt.getAnnotations());
for (final Annotation annotation : annotations) {
// XXX On purpose logic change to support both javax and jakarta namespace for annotations
if ("PostConstruct".equals(annotation.annotationType().getSimpleName())
&& mt.getParameterTypes().length == 0) {
// Invoke method newClass
mt.invoke(newClass);
// Invoke method expectedClass
mt.invoke(expectedClass);
}
}
}
// Invoke clear only on newClass
newClass.getClass().getMethod("clear").invoke(newClass);
Assertions.assertEquals(expectedClass, newClass,
String.format("Clear method does not match new object '%s'", this.clazz));
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
| NoSuchMethodException | SecurityException e) {
Assertions.fail(String.format("An exception was thrown while testing the Clear method '%s' : '%s'",
this.clazz.getName(), e.toString()));
}
}
}
}
/**
* Constructors test.
*/
void constructorsTest() {
for (final Constructor> constructor : this.clazz.getConstructors()) {
// Skip deprecated constructors
if (constructor.isAnnotationPresent(Deprecated.class)) {
continue;
}
final Class>[] types = constructor.getParameterTypes();
final Object[] values = new Object[constructor.getParameterTypes().length];
// Load Data
for (int i = 0; i < values.length; i++) {
values[i] = this.buildValue(types[i], LoadType.STANDARD_DATA);
}
try {
constructor.newInstance(values);
} catch (final InstantiationException | IllegalAccessException | InvocationTargetException e) {
Assertions.fail(
String.format("An exception was thrown while testing the constructor(s) '%s' with '%s': '%s'",
constructor.getName(), Arrays.toString(values), e.toString()));
}
// TODO 1/12/2019 JWL Add checking of new object properties
}
}
/**
* Check Serializable test.
*/
void checkSerializableTest() {
final T object = new ClassInstance().newInstance(this.clazz);
if (this.implementsSerializable(object)) {
final T newObject = this.canSerialize(object);
// Toggle to throw or not throw error with only one way working
if (this.skipStrictSerializable != SkipStrictSerialize.ON) {
Assertions.assertEquals(object, newObject);
} else {
Assertions.assertNotEquals(object, newObject);
}
return;
}
// Only throw error when specifically checking on serialization
if (this.checkSerializable == CheckSerialize.ON) {
Assertions.fail(String.format("Class is not serializable '%s'", object.getClass().getName()));
}
}
/**
* Implements serializable.
*
* @param object
* the object
*
* @return true, if successful
*/
boolean implementsSerializable(final T object) {
return object instanceof Serializable || object instanceof Externalizable;
}
/**
* Can serialize.
*
* @param object
* the object
*
* @return object read after serialization
*/
@SuppressWarnings("unchecked")
T canSerialize(final T object) {
// Serialize data
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
new ObjectOutputStream(baos).writeObject(object);
} catch (final IOException e) {
Assertions.fail(String.format("An exception was thrown while serializing the class '%s': '%s',",
object.getClass().getName(), e.toString()));
return null;
}
// Deserialize Data
final ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
try {
return (T) new ObjectInputStream(bais).readObject();
} catch (final ClassNotFoundException | IOException e) {
Assertions.fail(String.format("An exception was thrown while deserializing the class '%s': '%s',",
object.getClass().getName(), e.toString()));
}
return null;
}
/**
* Builds the value.
*
* @param
* the generic type
* @param returnType
* the return type
* @param loadType
* the load type
*
* @return the object
*/
private Object buildValue(final Class returnType, final LoadType loadType) {
final ValueBuilder valueBuilder = new ValueBuilder();
valueBuilder.setLoadData(this.loadData);
return valueBuilder.buildValue(returnType, loadType);
}
/**
* Tests the equals/hashCode/toString methods of the specified class.
*/
public void equalsHashCodeToStringSymmetricTest() {
// Run Equals Verifier
try {
EqualsVerifier.simple().forClass(this.clazz).verify();
} catch (AssertionError e) {
JavaBeanTesterWorker.LOGGER.warn("EqualsVerifier attempt failed: {}", e.getMessage());
}
// Create Instances
final T x = new ClassInstance().newInstance(this.clazz);
final T y = new ClassInstance().newInstance(this.clazz);
Assertions.assertNotNull(x,
String.format("Create new instance of class '%s' resulted in null", this.clazz.getName()));
Assertions.assertNotNull(y,
String.format("Create new instance of class '%s' resulted in null", this.clazz.getName()));
// TODO 1/12/2019 JWL Internalize extension will require canEquals, equals, hashcode, and toString overrides.
/*
* try { this.extension = (Class) new ExtensionBuilder().generate(this.clazz); } catch (NotFoundException
* e) { Assert.fail(e.getMessage()); } catch (CannotCompileException e) { Assert.fail(e.getMessage()); }
*/
final E ext = new ClassInstance().newInstance(this.extension);
Assertions.assertNotNull(ext,
String.format("Create new instance of extension %s resulted in null", this.extension.getName()));
// Test Equals, HashCode, and ToString on Empty Objects
Assertions.assertEquals(x, y,
String.format(".equals() should be consistent for two empty objects of type %s", this.clazz.getName()));
Assertions.assertEquals(x.hashCode(), y.hashCode(), String
.format(".hashCode() should be consistent for two empty objects of type %s", this.clazz.getName()));
Assertions.assertEquals(x.toString(), y.toString(), String
.format(".toString() should be consistent for two empty objects of type %s", this.clazz.getName()));
// Test Extension Equals, HashCode, and ToString on Empty Objects
Assertions.assertNotEquals(ext, y,
String.format(".equals() should not be equal for extension of type %s and empty object of type %s",
this.extension.getName(), this.clazz.getName()));
Assertions.assertNotEquals(ext.hashCode(), y.hashCode(),
String.format(".hashCode() should not be equal for extension of type %s and empty object of type %s",
this.extension.getName(), this.clazz.getName()));
Assertions.assertNotEquals(ext.toString(), y.toString(),
String.format(".toString() should not be equal for extension of type %s and empty object of type %s",
this.extension.getName(), this.clazz.getName()));
// Test One Sided Tests on Empty Objects
Assertions.assertNotEquals(x, null,
String.format("An empty object of type %s should not be equal to null", this.clazz.getName()));
Assertions.assertEquals(x, x,
String.format("An empty object of type %s should be equal to itself", this.clazz.getName()));
// Test Extension One Sided Tests on Empty Objects
Assertions.assertNotEquals(ext, null,
String.format("An empty extension of type %s should not be equal to null", this.clazz.getName()));
Assertions.assertEquals(ext, ext,
String.format("An empty extension of type %s should be equal to itself", this.extension.getName()));
// If the class has setters, the previous tests would have been against empty classes
// If so, load the classes and re-test
if (this.classHasSetters(this.clazz)) {
// Populate Side X
JavaBeanTesterWorker.load(this.clazz, x, this.loadData);
// Populate Extension Side Ext
JavaBeanTesterWorker.load(this.extension, ext, this.loadData);
// ReTest Equals (flip)
Assertions.assertNotEquals(y, x,
String.format(".equals() should not be consistent for one empty and one loaded object of type %s",
this.clazz.getName()));
// ReTest Extension Equals (flip)
Assertions.assertNotEquals(y, ext,
String.format(".equals() should not be equal for extension of type %s and empty object of type %s",
this.extension.getName(), this.clazz.getName()));
// Populate Size Y
JavaBeanTesterWorker.load(this.clazz, y, this.loadData);
// ReTest Equals and HashCode
if (this.loadData == LoadData.ON) {
Assertions.assertEquals(x, y,
String.format(".equals() should be equal for two instances of type %s with loaded data",
this.clazz.getName()));
Assertions.assertEquals(x.hashCode(), y.hashCode(),
String.format(".hashCode() should be equal for two instances of type %s with loaded data",
this.clazz.getName()));
} else {
Assertions.assertNotEquals(x, y);
Assertions.assertNotEquals(x.hashCode(), y.hashCode());
}
// ReTest Extension Equals, HashCode, and ToString
Assertions.assertNotEquals(ext, y,
String.format(".equals() should not be equal for extension of type %s and empty object of type %s",
this.extension.getName(), this.clazz.getName()));
Assertions.assertNotEquals(ext.hashCode(), y.hashCode(),
String.format(
".hashCode() should not be equal for extension of type %s and empty object of type %s",
this.extension.getName(), this.clazz.getName()));
Assertions.assertNotEquals(ext.toString(), y.toString(),
String.format(
".toString() should not be equal for extension of type %s and empty object of type %s",
this.extension.getName(), this.clazz.getName()));
}
// Create Immutable Instance
try {
final BeanCopier clazzBeanCopier = BeanCopier.create(this.clazz, this.clazz, true);
final T e = new ClassInstance().newInstance(this.clazz);
clazzBeanCopier.copy(x, e, null);
Assertions.assertEquals(e, x);
} catch (final Exception e) {
JavaBeanTesterWorker.LOGGER.trace("Do nothing class is not mutable", e);
}
// Create Extension Immutable Instance
try {
final BeanCopier extensionBeanCopier = BeanCopier.create(this.extension, this.extension, true);
final E e = new ClassInstance().newInstance(this.extension);
extensionBeanCopier.copy(ext, e, null);
Assertions.assertEquals(e, ext);
} catch (final Exception e) {
JavaBeanTesterWorker.LOGGER.trace("Do nothing class is not mutable", e);
}
}
/**
* Equals Tests will traverse one object changing values until all have been tested against another object. This is
* done to effectively test all paths through equals.
*
* @param instance
* the class instance under test.
* @param expected
* the instance expected for tests.
*/
void equalsTests(final T instance, final T expected) {
// Perform hashCode test dependent on data coming in
// Assert.assertEquals(expected.hashCode(), instance.hashCode());
if (expected.hashCode() == instance.hashCode()) {
Assertions.assertEquals(expected.hashCode(), instance.hashCode());
} else {
Assertions.assertNotEquals(expected.hashCode(), instance.hashCode());
}
final ValueBuilder valueBuilder = new ValueBuilder();
valueBuilder.setLoadData(this.loadData);
final PropertyDescriptor[] props = this.getProps(instance.getClass());
for (final PropertyDescriptor prop : props) {
Method getter = prop.getReadMethod();
final Method setter = prop.getWriteMethod();
// Java Metro Bug Patch (Boolean Wrapper usage of 'is' possible
if (getter == null && setter != null) {
final String isBooleanWrapper = "is" + setter.getName().substring(3);
try {
getter = this.clazz.getMethod(isBooleanWrapper);
} catch (NoSuchMethodException | SecurityException e) {
// Do nothing
}
}
if (getter != null && setter != null) {
// We have both a get and set method for this property
final Class> returnType = getter.getReturnType();
final Class>[] params = setter.getParameterTypes();
if (params.length == 1 && params[0] == returnType) {
// The set method has 1 argument, which is of the same type as the return type of the get method, so
// we can test this property
try {
// Save original value
final Object original = getter.invoke(instance);
// Build a value of the correct type to be passed to the set method using alternate test
Object value = valueBuilder.buildValue(returnType, LoadType.ALTERNATE_DATA);
// Call the set method, then check the same value comes back out of the get method
setter.invoke(instance, value);
// Check equals depending on data
if (instance.equals(expected)) {
Assertions.assertEquals(expected, instance);
} else {
Assertions.assertNotEquals(expected, instance);
}
// Build a value of the correct type to be passed to the set method using null test
value = valueBuilder.buildValue(returnType, LoadType.NULL_DATA);
// Call the set method, then check the same value comes back out of the get method
setter.invoke(instance, value);
// Check equals depending on data
if (instance.equals(expected)) {
Assertions.assertEquals(expected, instance);
} else {
Assertions.assertNotEquals(expected, instance);
}
// Reset to original value
setter.invoke(instance, original);
} catch (final IllegalAccessException | IllegalArgumentException | InvocationTargetException
| SecurityException e) {
Assertions.fail(
String.format("An exception was thrown while testing the property (equals) '%s': '%s'",
prop.getName(), e.toString()));
}
}
}
}
}
/**
* Class has setters.
*
* @param clazz
* the clazz
*
* @return true, if successful
*/
private boolean classHasSetters(final Class clazz) {
return Arrays.stream(this.getProps(clazz))
.anyMatch(propertyDescriptor -> propertyDescriptor.getWriteMethod() != null);
}
/**
* Gets the props.
*
* @param clazz
* the clazz
*
* @return the props
*/
private PropertyDescriptor[] getProps(final Class> clazz) {
try {
final List usedProps = new ArrayList<>(
Introspector.getBeanInfo(clazz).getPropertyDescriptors().length);
final List props = Arrays
.asList(Introspector.getBeanInfo(clazz).getPropertyDescriptors());
nextProp: for (final PropertyDescriptor prop : props) {
// Check the list of properties that we don't want to test
for (final String skipThis : this.skipThese) {
if (skipThis.equals(prop.getName())) {
continue nextProp;
}
}
usedProps.add(prop);
}
return usedProps.toArray(new PropertyDescriptor[usedProps.size()]);
} catch (final IntrospectionException e) {
Assertions.fail(String.format("An exception was thrown while testing class '%s': '%s'",
this.clazz.getName(), e.toString()));
return new PropertyDescriptor[0];
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy