org.joda.beans.test.BeanAssert Maven / Gradle / Ivy
/*
* Copyright 2001-present Stephen Colebourne
*
* 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.joda.beans.test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.joda.beans.Bean;
import org.joda.beans.JodaBeanUtils;
import org.joda.beans.MetaProperty;
/**
* Assertion class to compare beans.
*
* This class fulfils a similar role to other assertion libraries in testing code.
* It should generally be statically imported.
*/
public final class BeanAssert {
/**
* Restricted constructor.
*/
private BeanAssert() {
}
//-----------------------------------------------------------------------
/**
* Asserts that two beans are equal, providing a better error message.
*
* @param expected the expected value, not null
* @param actual the actual value, not null
*/
public static void assertBeanEquals(Bean expected, Bean actual) {
assertBeanEquals(null, expected, actual, 0d);
}
/**
* Asserts that two beans are equal, providing a better error message.
*
* Note that specifying a tolerance can mean that two beans compare as not
* equal using {@link Object#equals(Object)} but equal using this method,
* because the standard equals method has no tolerance.
*
* @param expected the expected value, not null
* @param actual the actual value, not null
* @param tolerance the tolerance to use for {@code double} and {@code float}
*/
public static void assertBeanEquals(Bean expected, Bean actual, double tolerance) {
assertBeanEquals(null, expected, actual, tolerance);
}
/**
* Asserts that two beans are equal, providing a better error message.
*
* @param baseMsg the message to use in any error, null uses default message
* @param expected the expected value, not null
* @param actual the actual value, not null
*/
public static void assertBeanEquals(String baseMsg, Bean expected, Bean actual) {
assertBeanEquals(baseMsg, expected, actual, 0d);
}
/**
* Asserts that two beans are equal, providing a better error message.
*
* Note that specifying a tolerance can mean that two beans compare as not
* equal using {@link Object#equals(Object)} but equal using this method,
* because the standard equals method has no tolerance.
*
* @param baseMsg the message to use in any error, null uses default message
* @param expected the expected value, not null
* @param actual the actual value, not null
* @param tolerance the tolerance to use for {@code double} and {@code float}
*/
public static void assertBeanEquals(String baseMsg, Bean expected, Bean actual, double tolerance) {
if (expected == null) {
throw new AssertionError(baseMsg + ": Expected bean must not be null");
}
if (actual == null) {
throw new AssertionError(baseMsg + ": Actual bean must not be null");
}
if (expected.equals(actual) == false) {
String comparisonMsg = buildMessage(baseMsg, 10, expected, actual, tolerance);
if (comparisonMsg.isEmpty()) {
return; // no errors, just double/float within tolerance
}
throw new BeanComparisonError(comparisonMsg, expected, actual);
}
}
//-----------------------------------------------------------------------
/**
* Asserts that two beans are equal, providing a better error message.
*
* @param expected the expected value, not null
* @param actual the actual value, not null
*/
public static void assertBeanEqualsFullDetail(Bean expected, Bean actual) {
assertBeanEqualsFullDetail(null, expected, actual, 0d);
}
/**
* Asserts that two beans are equal, providing a better error message.
*
* Note that specifying a tolerance can mean that two beans compare as not
* equal using {@link Object#equals(Object)} but equal using this method,
* because the standard equals method has no tolerance.
*
* @param expected the expected value, not null
* @param actual the actual value, not null
* @param tolerance the tolerance to use for {@code double} and {@code float}
*/
public static void assertBeanEqualsFullDetail(Bean expected, Bean actual, double tolerance) {
assertBeanEqualsFullDetail(null, expected, actual, tolerance);
}
/**
* Asserts that two beans are equal, providing a better error message, with
* an unlimited number of errors reported.
*
* @param baseMsg the message to use in any error, null uses default message
* @param expected the expected value, not null
* @param actual the actual value, not null
*/
public static void assertBeanEqualsFullDetail(String baseMsg, Bean expected, Bean actual) {
assertBeanEqualsFullDetail(baseMsg, expected, actual, 0d);
}
/**
* Asserts that two beans are equal, providing a better error message, with
* an unlimited number of errors reported.
*
* Note that specifying a tolerance can mean that two beans compare as not
* equal using {@link Object#equals(Object)} but equal using this method,
* because the standard equals method has no tolerance.
*
* @param baseMsg the message to use in any error, null uses default message
* @param expected the expected value, not null
* @param actual the actual value, not null
* @param tolerance the tolerance to use for {@code double} and {@code float}
*/
public static void assertBeanEqualsFullDetail(String baseMsg, Bean expected, Bean actual, double tolerance) {
if (expected == null) {
throw new AssertionError(baseMsg + ": Expected bean must not be null");
}
if (actual == null) {
throw new AssertionError(baseMsg + ": Actual bean must not be null");
}
if (expected.equals(actual) == false) {
String comparisonMsg = buildMessage(baseMsg, Integer.MAX_VALUE, expected, actual, tolerance);
if (comparisonMsg.isEmpty()) {
return; // no errors, just double/float within tolerance
}
throw new BeanComparisonError(comparisonMsg, expected, actual);
}
}
//-----------------------------------------------------------------------
/**
* Compares the two beans.
*
* @param baseMsg the message, may be null
* @param maxErrors the maximum number of errors to report
* @param expected the expected value, not null
* @param actual the actual value, not null
* @param tolerance the tolerance to use for {@code double} and {@code float}
* @return the message, not null
*/
private static String buildMessage(String baseMsg, int maxErrors, Bean expected, Bean actual, double tolerance) {
List diffs = new ArrayList<>();
buildMessage(diffs, "", expected, actual, tolerance);
if (diffs.size() == 0) {
return "";
}
StringBuilder buf = new StringBuilder();
buf.append(baseMsg != null ? baseMsg + ": " : "");
buf.append("Bean did not equal expected. Differences:");
int size = diffs.size();
if (size > maxErrors) {
diffs = diffs.subList(0, maxErrors);
}
for (String diff : diffs) {
buf.append('\n').append(diff);
}
if (size > maxErrors) {
buf.append("\n...and " + (size - 10) + " more differences");
}
return buf.toString();
}
private static void buildMessage(List diffs, String prefix, Object expected, Object actual, double tolerance) {
if (expected == null && actual == null) {
return;
}
if (expected == null && actual != null) {
diffs.add(prefix + ": Expected null, but was " + buildSummary(actual, true));
return;
}
if (expected != null && actual == null) {
diffs.add(prefix + ": Was null, but expected " + buildSummary(expected, true));
return;
}
if (expected instanceof List && actual instanceof List) {
List> expectedList = (List>) expected;
List> actualList = (List>) actual;
if (expectedList.size() != actualList.size()) {
diffs.add(prefix + ": List size differs, expected " + expectedList.size() + " but was " + actualList.size());
return;
}
for (int i = 0; i < expectedList.size(); i++) {
buildMessage(diffs, prefix + '[' + i + "]", expectedList.get(i), actualList.get(i), tolerance);
}
return;
}
if (expected instanceof Map && actual instanceof Map) {
Map, ?> expectedMap = (Map, ?>) expected;
Map, ?> actualMap = (Map, ?>) actual;
if (expectedMap.size() != actualMap.size()) {
diffs.add(prefix + ": Map size differs, expected " + expectedMap.size() + " but was " + actualMap.size());
return;
}
if (expectedMap.keySet().equals(actualMap.keySet()) == false) {
diffs.add(prefix + ": Map keyset differs, expected " + buildSummary(expectedMap.keySet(), false) + " but was " + buildSummary(actualMap.keySet(), false));
return;
}
for (Object key : expectedMap.keySet()) {
buildMessage(diffs, prefix + '[' + key + "]", expectedMap.get(key), actualMap.get(key), tolerance);
}
return;
}
if (expected != null && expected.getClass() != actual.getClass()) {
diffs.add(prefix + ": Class differs, expected " + buildSummary(expected, true) + " but was " + buildSummary(actual, true));
return;
}
if (expected instanceof Bean) {
for (MetaProperty> prop : ((Bean) expected).metaBean().metaPropertyIterable()) {
buildMessage(diffs, prefix + '.' + prop.name(), prop.get((Bean) expected), prop.get((Bean) actual), tolerance);
}
return;
}
if (expected instanceof Double && actual instanceof Double && tolerance != 0d) {
double e = (Double) expected;
double a = (Double) actual;
if (!JodaBeanUtils.equalWithTolerance(e, a, tolerance)) {
diffs.add(prefix + ": Double values differ by more than allowed tolerance, expected " +
buildSummary(expected, true) + " but was " + buildSummary(actual, false));
}
return;
}
if (expected instanceof double[] && actual instanceof double[] && tolerance != 0d) {
double[] e = (double[]) expected;
double[] a = (double[]) actual;
if (e.length != a.length) {
diffs.add(prefix + ": Double arrays differ in length, expected " +
buildSummary(expected, true) + " but was " + buildSummary(actual, false));
} else {
for (int i = 0; i < a.length; i++) {
if (!JodaBeanUtils.equalWithTolerance(e[i], a[i], tolerance)) {
diffs.add(prefix + ": Double arrays differ by more than allowed tolerance, expected " +
buildSummary(expected, true) + " but was " + buildSummary(actual, false));
break;
}
}
}
return;
}
if (expected instanceof Float && actual instanceof Float && tolerance != 0d) {
float e = (Float) expected;
float a = (Float) actual;
if (!JodaBeanUtils.equalWithTolerance(e, a, tolerance)) {
diffs.add(prefix + ": Float values differ by more than allowed tolerance, expected " +
buildSummary(expected, true) + " but was " + buildSummary(actual, false));
}
return;
}
if (expected instanceof float[] && actual instanceof float[] && tolerance != 0d) {
float[] e = (float[]) expected;
float[] a = (float[]) actual;
if (e.length != a.length) {
diffs.add(prefix + ": Double arrays differ in length, expected " +
buildSummary(expected, true) + " but was " + buildSummary(actual, false));
} else {
for (int i = 0; i < a.length; i++) {
if (!JodaBeanUtils.equalWithTolerance(e[i], a[i], tolerance)) {
diffs.add(prefix + ": Float arrays differ by more than allowed tolerance, expected " +
buildSummary(expected, true) + " but was " + buildSummary(actual, false));
break;
}
}
}
return;
}
if (JodaBeanUtils.equal(expected, actual) == false) {
diffs.add(prefix + ": Content differs, expected " + buildSummary(expected, true) + " but was " + buildSummary(actual, false));
return;
}
return; // equal
}
/**
* Builds a summary of an object.
*
* @param obj the object to summarise, not null
*/
private static String buildSummary(Object obj, boolean includeType) {
String type = obj.getClass().getSimpleName();
String toStr;
if (obj instanceof double[]) {
toStr = Arrays.toString((double[]) obj);
} else if (obj instanceof float[]) {
toStr = Arrays.toString((float[]) obj);
} else {
toStr = obj.toString();
}
if (toStr.length() > 60) {
toStr = toStr.substring(0, 57) + "...";
}
return (includeType ? type + " " : "") + "<" + toStr + ">";
}
}