IsActualEqualToExpectedJsonValueComparer.java
package org.dbunit.assertion.comparer.value;
import java.io.IOException;
import org.dbunit.DatabaseUnitException;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.datatype.DataType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* {@link ValueComparer} implementation that verifies the actual value is
* semantically equal to the expected value by parsing both as JSON and
* comparing the resulting document trees, instead of comparing their raw
* text.
*
* <p>Databases that store native JSON (for example MySQL {@code JSON},
* PostgreSQL {@code json}/{@code jsonb}, or H2 {@code JSON}) commonly
* reformat the text on storage: insignificant whitespace is stripped and
* object keys may be reordered. A plain string or
* {@link DataType#compare(Object, Object)} comparison then fails even when
* the expected and actual documents are equivalent. This comparer instead
* treats JSON object member order as insignificant while still treating JSON
* array element order as significant, matching JSON's own equality
* semantics. Special case: if both values are null, they match.
*
* <p>Requires the optional {@code jackson-databind} dependency (the same one
* used by {@link org.dbunit.dataset.json.JsonDataSet}) on the classpath.
* Deliberately not exposed as a constant on {@link ValueComparers}, because
* that class eagerly instantiates every constant it declares; doing so here
* would force the optional dependency onto every consumer of
* {@link ValueComparers}, not only those comparing JSON columns. Construct
* this comparer directly instead.
*
* @author Jeff Jensen
* @since 3.5.0
*/
public class IsActualEqualToExpectedJsonValueComparer
extends ValueComparerTemplateBase
{
private final Logger log = LoggerFactory.getLogger(getClass());
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Override
protected boolean isExpected(final ITable expectedTable,
final ITable actualTable, final int rowNum, final String columnName,
final DataType dataType, final Object expectedValue,
final Object actualValue) throws DatabaseUnitException
{
final boolean isExpected;
// handle nulls: prevent NPE and isExpected=true when both null
if (expectedValue == null && actualValue == null)
{
// both are null, so match
isExpected = true;
} else if (expectedValue == null || actualValue == null)
{
// both aren't null, one is null, so no match
isExpected = false;
} else
{
// neither are null, so compare
isExpected = isJsonEqual(rowNum, columnName, expectedValue,
actualValue);
}
return isExpected;
}
/**
* Returns whether the expected and actual values parse as structurally
* equal JSON documents.
*
* @param rowNum the current row number comparing, used only to identify a parse failure.
* @param columnName the name of the current column comparing, used only to identify a parse failure.
* @param expectedValue the expected value.
* @param actualValue the actual value.
* @return <code>true</code> if both values parse as JSON and their document trees are equal.
* @throws DatabaseUnitException if either value cannot be converted to a string or parsed as JSON.
*/
protected boolean isJsonEqual(final int rowNum, final String columnName,
final Object expectedValue, final Object actualValue)
throws DatabaseUnitException
{
final JsonNode expectedNode =
parseJson(rowNum, columnName, "expected", expectedValue);
final JsonNode actualNode =
parseJson(rowNum, columnName, "actual", actualValue);
log.debug("isJsonEqual: expectedNode={}, actualNode={}", expectedNode,
actualNode);
return actualNode.equals(expectedNode);
}
private JsonNode parseJson(final int rowNum, final String columnName,
final String label, final Object value) throws DatabaseUnitException
{
final String json = DataType.asString(value);
final JsonNode node;
try
{
node = OBJECT_MAPPER.readTree(json);
} catch (final IOException e)
{
throw new DatabaseUnitException(
parseFailureMessage(rowNum, columnName, label, json), e);
}
if (node == null || node.isMissingNode())
{
// Jackson returns a MissingNode (not an exception, and not a
// Java null either) for empty or whitespace-only input
throw new DatabaseUnitException(
parseFailureMessage(rowNum, columnName, label, json));
}
return node;
}
private String parseFailureMessage(final int rowNum,
final String columnName, final String label, final String json)
{
return String.format(
"Unable to parse %s value as JSON for column '%s', row %d: %s",
label, columnName, rowNum, json);
}
@Override
protected String getFailPhrase()
{
return "not JSON-equal to";
}
}