org.meanbean.test.PropertyBasedEqualsMethodPropertySignificanceVerifier Maven / Gradle / Ivy
Show all versions of meanbean Show documentation
/*-
*
* meanbean
*
* Copyright (C) 2010 - 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.meanbean.test;
import org.meanbean.bean.info.BeanInformation;
import org.meanbean.bean.info.BeanInformationException;
import org.meanbean.bean.info.BeanInformationFactory;
import org.meanbean.bean.info.PropertyInformation;
import org.meanbean.bean.util.PropertyInformationFilter;
import org.meanbean.factories.util.FactoryLookupStrategy;
import org.meanbean.lang.EquivalentFactory;
import org.meanbean.lang.Factory;
import org.meanbean.util.ValidationHelper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
*
* Concrete EqualsMethodPropertySignificanceVerifier implementation that affords functionality to verify that the equals
* logic implemented by a type is affected in the expected manner when changes are made to the property values of
* instances of the type.
*
*
*
* That is:
*
*
*
* - the equality of an object should not be affected by properties that are changed, but are not considered in the
* equality logic
*
* - the equality of an object should be affected by properties that are changed and are considered in the equality
* logic
*
*
*
* To do this, instances of the type are created using a specified factory, their properties are manipulated
* individually and the equality is reassessed.
*
*
*
* For the test to function correctly, you must specify all properties that are not used in the equals logic
* (insignificant
).
*
*
*
* Use verifyEquals()
to test a class that overrides equals()
.
*
*
*
* As an example, to verify the equals logic implemented by a class called MyClass do the following:
*
*
*
* EqualsMethodPropertySignificanceVerifier verifier = new PropertyBasedEqualsMethodPropertySignificanceVerifier();
* verifier.verifyEqualsMethod(new Factory() {
* @Override
* public MyClass create() {
* MyClass() result = new MyClass();
* // initialize result...
* result.setName("TEST_NAME");
* return result;
* }
* });
*
*
*
* The Factory creates new logically equivalent instances of MyClass. MyClass has overridden
* equals()
and hashCode()
. In the above example, there is only one property, name, which is
* considered by MyClass's equals logic.
*
*
*
* The following example tests the equals logic implemented by a class called MyComplexClass which has two properties:
* firstName and lastName. Only firstName is considered in the equals logic. Therefore, lastName is specified in the
* insignificantProperties varargs:
*
*
*
* EqualsMethodPropertySignificanceVerifier verifier = new PropertyBasedEqualsMethodPropertySignificanceVerifier();
* verifier.verifyEqualsMethod(new Factory() {
* @Override
* public MyComplexClass create() {
* MyComplexClass() result = new MyComplexClass();
* // initialize result...
* result.setFirstName("TEST_FIRST_NAME");
* result.setLastName("TEST_LAST_NAME");
* return result;
* }
* }, "lastName");
*
*
* @author Graham Williamson
*/
class PropertyBasedEqualsMethodPropertySignificanceVerifier implements EqualsMethodPropertySignificanceVerifier {
/** Factory used to gather information about a given bean and store it in a BeanInformation object. */
private final BeanInformationFactory beanInformationFactory = BeanInformationFactory.getInstance();
/** Provides a means of acquiring a suitable Factory. */
private final FactoryLookupStrategy factoryLookupStrategy = FactoryLookupStrategy.getInstance();
/** Asserts that the equality logic is consistent for a significant property. */
private final ObjectPropertyEqualityConsistentAsserter significantAsserter =
new SignificantObjectPropertyEqualityConsistentAsserter();
/** Asserts that the equality logic is consistent for an insignificant property. */
private final ObjectPropertyEqualityConsistentAsserter insignificantAsserter =
new InsignificantObjectPropertyEqualityConsistentAsserter();
/**
*
* Verify that the equals logic implemented by the type the specified factory creates is affected in the expected
* manner when changes are made to the property values of instances of the type.
*
*
*
* That is:
*
*
*
* - the equality of an object should not be affected by properties that are changed, but are not considered in
* the equality logic
*
* - the equality of an object should be affected by properties that are changed and are considered in the
* equality logic
*
*
*
* To do this, instances of the type are created using the specified factory, their properties are manipulated
* individually and the equality is reassessed.
*
*
*
* For the test to function correctly, you must specify all properties that are not used in the equals logic.
*
*
*
* If the test fails, an AssertionError is thrown.
*
*
* @param factory
* An EquivalentFactory that creates non-null logically equivalent objects that will be used to test the
* equals logic. The factory must create logically equivalent but different actual instances of the type
* upon each invocation of create()
in order for the test to be meaningful.
* @param insignificantProperties
* The names of properties that are not used when deciding whether objects are logically equivalent. For
* example, "lastName".
*
* @throws IllegalArgumentException
* If either the specified factory or insignificantProperties are deemed illegal. For example, if either
* is null
. Also, if any of the specified insignificantProperties do not exist on the class
* under test.
* @throws BeanInformationException
* If a problem occurs when trying to obtain information about the type to test.
* @throws BeanTestException
* If a problem occurs when testing the type, such as an inability to read or write a property of the
* type to test.
* @throws AssertionError
* If the test fails.
*/
@Override
public void verifyEqualsMethod(EquivalentFactory> factory, String... insignificantProperties)
throws IllegalArgumentException, BeanInformationException, BeanTestException, AssertionError {
verifyEqualsMethod(factory, null, insignificantProperties);
}
/**
*
* Verify that the equals logic implemented by the type the specified factory creates is affected in the expected
* manner when changes are made to the property values of instances of the type.
*
*
*
* That is:
*
*
*
* - the equality of an object should not be affected by properties that are changed, but are not considered in
* the equality logic
*
* - the equality of an object should be affected by properties that are changed and are considered in the
* equality logic
*
*
*
* To do this, instances of the type are created using the specified factory, their properties are manipulated
* individually and the equality is reassessed.
*
*
*
* For the test to function correctly, you must specify all properties that are not used in the equals logic.
*
*
*
* If the test fails, an AssertionError is thrown.
*
*
* @param factory
* An EquivalentFactory that creates non-null logically equivalent objects that will be used to test the
* equals logic. The factory must create logically equivalent but different actual instances of the type
* upon each invocation of create()
in order for the test to be meaningful.
* @param customConfiguration
* A custom Configuration to be used when testing to ignore the testing of named properties or use a
* custom test data Factory when testing a named property. This Configuration is only used for this
* individual test and will not be retained for future testing of this or any other type. If no custom
* Configuration is required, pass null
or use
* verifyEqualsMethod(Factory>,String...)
instead.
* @param insignificantProperties
* The names of properties that are not used when deciding whether objects are logically equivalent. For
* example, "lastName".
*
* @throws IllegalArgumentException
* If either the specified factory or insignificantProperties are deemed illegal. For example, if either
* is null
. Also, if any of the specified insignificantProperties do not exist on the class
* under test.
* @throws BeanInformationException
* If a problem occurs when trying to obtain information about the type to test.
* @throws BeanTestException
* If a problem occurs when testing the type, such as an inability to read or write a property of the
* type to test.
* @throws AssertionError
* If the test fails.
*/
@Override
public void verifyEqualsMethod(EquivalentFactory> factory, Configuration customConfiguration,
String... insignificantProperties) throws IllegalArgumentException, BeanInformationException,
BeanTestException, AssertionError {
ValidationHelper.ensureExists("factory", "test equals", factory);
ValidationHelper.ensureExists("insignificantProperties", "test equals", insignificantProperties);
List insignificantPropertyNames = new ArrayList<>();
insignificantPropertyNames.addAll(Arrays.asList(insignificantProperties));
if (customConfiguration != null) {
insignificantPropertyNames.addAll(customConfiguration.getEqualsInsignificantProperties());
}
Object prototype = factory.create();
ValidationHelper.ensureExists("factory-created object", "test equals", prototype);
BeanInformation beanInformation = beanInformationFactory.create(prototype.getClass());
ensureInsignificantPropertiesExist(beanInformation, insignificantPropertyNames);
Collection properties = beanInformation.getProperties();
properties = PropertyInformationFilter.filter(beanInformation.getProperties(), customConfiguration);
for (PropertyInformation property : properties) {
verifyEqualsMethodForProperty(beanInformation, factory, customConfiguration, property,
!insignificantPropertyNames.contains(property.getName()));
}
}
/**
* Ensure that all of the specified insignificant properties exist on the specified bean. If an insignificant
* property is specified that does not exist on the bean, an IllegalArgumentException
is thrown.
*
* @param beanInformation
* Information about the bean on which all of the insignificant properties should exist.
* @param insignificantProperties
* The names of the insignificant properties that should exist on the bean.
*
* @throws IllegalArgumentException
* If an insignificant property is specified that does not exist on the specified bean.
*/
protected void ensureInsignificantPropertiesExist(BeanInformation beanInformation,
List insignificantProperties) throws IllegalArgumentException {
List unrecognisedPropertyNames = new ArrayList(insignificantProperties);
unrecognisedPropertyNames.removeAll(beanInformation.getPropertyNames());
if (!unrecognisedPropertyNames.isEmpty()) {
String message =
"Insignificant properties [" + String.join(",", unrecognisedPropertyNames)
+ "] do not exist on " + beanInformation.getBeanClass().getName() + ".";
throw new IllegalArgumentException(message);
}
}
/**
*
* Verify that the equals logic implemented by the type the specified factory creates is affected in the expected
* manner when changes are made to the value of the specified property.
*
*
*
* That is:
*
*
*
* - the equality of an object should not be affected by properties that are changed, but are not considered in
* the equality logic
*
* - the equality of an object should be affected by properties that are changed and are considered in the
* equality logic
*
*
*
* To do this, instances of the type are created using the specified factory, the specified property is manipulated
* and the equality is reassessed.
*
*
*
* For the test to function correctly, you must specify whether the property is used in the equals logic.
*
*
*
* If the test fails, an AssertionError is thrown.
*
*
* @param beanInformation
* Information about the bean the property belongs to.
* @param factory
* An EquivalentFactory that creates non-null logically equivalent objects that will be used to test the
* equals logic. The factory must create logically equivalent but different actual instances of the type
* upon each invocation of create()
in order for the test to be meaningful.
* @param configuration
* A custom Configuration to be used when testing to ignore the testing of named properties or use a
* custom test data Factory when testing a named property. This Configuration is only used for this
* individual test and will not be retained for future testing of this or any other type. If no custom
* Configuration is required, pass null
.
* @param property
* The property to test.
* @param significant
* Set to true
if the property is used when deciding whether objects are logically
* equivalent; set to false
if the property is not used when deciding whether objects are
* logically equivalent.
*
* @throws IllegalArgumentException
* If any of the parameters are deemed illegal. For example, if any are null
(except
* configuration, which can be null
).
* @throws BeanInformationException
* If a problem occurs when trying to obtain information about the type to test.
* @throws BeanTestException
* If a problem occurs when testing the property, such as an inability to read or write the property.
* @throws AssertionError
* If the test fails.
*/
protected void verifyEqualsMethodForProperty(BeanInformation beanInformation, EquivalentFactory> factory,
Configuration configuration, PropertyInformation property, boolean significant)
throws IllegalArgumentException, BeanInformationException, BeanTestException, AssertionError {
String propertyName = property.getName();
Object originalObj = factory.create();
Object modifiedObj = factory.create();
if (!originalObj.equals(modifiedObj)) {
String message = "Cannot test equals if factory does not create logically equivalent objects.";
throw new IllegalArgumentException(message);
}
try {
Object xOriginalValue = property.getReadMethod().invoke(originalObj);
Object originalVal = property.getReadMethod().invoke(modifiedObj);
ValidationHelper.ensureExists("factory-created object." + propertyName, "test equals", xOriginalValue);
ValidationHelper.ensureExists("factory-created object." + propertyName, "test equals", originalVal);
if (!originalVal.equals(xOriginalValue)) {
String message = "Cannot test equals if factory does not create objects with same property values.";
throw new IllegalArgumentException(message);
}
Factory> propertyFactory = factoryLookupStrategy.getFactory(beanInformation, property, configuration);
Object newVal = propertyFactory.create();
property.getWriteMethod().invoke(modifiedObj, newVal);
if (significant) {
significantAsserter.assertConsistent(propertyName, originalObj, modifiedObj, originalVal, newVal);
} else {
insignificantAsserter.assertConsistent(propertyName, originalObj, modifiedObj, originalVal, newVal);
}
} catch (Exception e) {
if (e instanceof IllegalArgumentException) {
throw (IllegalArgumentException) e; // re-throw without wrapping
}
String message =
"Failed to test property [" + property.getName() + "] due to Exception [" + e.getClass().getName()
+ "]: [" + e.getMessage() + "].";
throw new BeanTestException(message, e);
}
}
}