Skip to content

Instantly share code, notes, and snippets.

@sleberknight
Last active August 6, 2021 21:19
Show Gist options
  • Save sleberknight/b112926d51da98617161ade108fb4450 to your computer and use it in GitHub Desktop.
Save sleberknight/b112926d51da98617161ade108fb4450 to your computer and use it in GitHub Desktop.
Playing around with XmlUnit and XmlAssert to log all differences
package org.kiwiproject.test.playground;
import static java.util.Objects.nonNull;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.kiwiproject.test.util.Fixtures;
import org.xmlunit.assertj3.CompareAssert;
import org.xmlunit.assertj3.XmlAssert;
import org.xmlunit.diff.Comparison;
import org.xmlunit.diff.ComparisonControllers;
import org.xmlunit.diff.ComparisonListener;
import org.xmlunit.diff.ComparisonResult;
import java.util.concurrent.atomic.AtomicBoolean;
@DisplayName("Using XmlUnit to log all the differences")
class XmlUnitLogAllDifferencesTest {
// This passes
@Test
void shouldCompareIdentical() {
var alice1 = Fixtures.fixture("alice-smith-1.xml");
var alice2 = Fixtures.fixture("alice-smith-2.xml");
XmlAssert.assertThat(alice1).and(alice2).areIdentical();
}
// This passes
@Test
void shouldCompareCaseInsensitive() {
var alice1 = Fixtures.fixture("alice-smith-1.xml");
var alice2 = Fixtures.fixture("alice-smith-3.xml");
XmlAssert.assertThat(alice1).and(alice2).ignoreWhitespace().areIdentical();
}
// This FAILS and logs ALL the differences (does not include test name in log output)
@Test
void shouldLogAllDifferencesUsingLoggingComparisonListener() {
var aliceSmith = Fixtures.fixture("alice-smith-1.xml");
var aliceJones = Fixtures.fixture("alice-jones.xml");
XmlAssert.assertThat(aliceSmith)
.and(aliceJones)
.withComparisonController(ComparisonControllers.Default)
.withComparisonListeners(new LoggingComparisonListener())
.areIdentical();
}
// This FAILS and logs ALL the differences (includes test name in log output)
@Test
void shouldLogAllDifferencesAndTestNameUsingLoggingComparisonListener(TestInfo testInfo) {
var aliceSmith = Fixtures.fixture("alice-smith-1.xml");
var aliceJones = Fixtures.fixture("alice-jones.xml");
XmlAssert.assertThat(aliceSmith)
.and(aliceJones)
.withComparisonController(ComparisonControllers.Default)
.withComparisonListeners(new LoggingComparisonListener(testInfo))
.areIdentical();
}
// This FAILS and logs ALL the differences using the simple OpinionatedXmlAssert (includes test name in log output)
@Test
void shouldUseOpinionatedXmlAssertToHideTheDetails(TestInfo testInfo) {
var aliceSmith = Fixtures.fixture("alice-smith-1.xml");
var aliceJones = Fixtures.fixture("alice-jones.xml");
OpinionatedXmlAssert.assertThat(aliceSmith).withTestNameFrom(testInfo).and(aliceJones).areIdentical();
}
/**
* Sits on top of {@link XmlAssert} to provide a simple way to compare all differences and log them as they occur.
* <p>
* This matters only because by default {@link XmlAssert} stops comparing when it finds the first difference, and
* only reports that difference. So if an XML comparison has multiple differences, you will only see the first
* one and won't know about any others until you've fixed the first one, then re-run the test, and then continue
* until all other comparison failures are fixed.
*/
static class OpinionatedXmlAssert {
private final Object o;
private String testName;
private OpinionatedXmlAssert(Object o) {
this.o = o;
}
// Entry point
static OpinionatedXmlAssert assertThat(Object o) {
return new OpinionatedXmlAssert(o);
}
// Optionally include the test name in the logging output when there are differences
OpinionatedXmlAssert withTestNameFrom(TestInfo testInfo) {
return withTestName(testInfo.getDisplayName());
}
// Optionally include the test name in the logging output when there are differences
OpinionatedXmlAssert withTestName(String testName) {
this.testName = testName;
return this;
}
// Terminal operation in OpinionatedXmlAssert; you get a CompareAssert that has been configured with the
// "opinionated" settings of using the default ComparisonController and the LoggingComparisonListener
CompareAssert and(Object control) {
return XmlAssert.assertThat(o)
.and(control)
.withComparisonController(ComparisonControllers.Default)
.withComparisonListeners(new LoggingComparisonListener(testName));
}
}
/**
* Logs differences in XML comparison results.
*/
@Slf4j
static class LoggingComparisonListener implements ComparisonListener {
private final String testName;
private final AtomicBoolean haveSeenDifferenceAlready;
LoggingComparisonListener() {
this((String) null);
}
LoggingComparisonListener(TestInfo testInfo) {
this(testInfo.getDisplayName());
}
LoggingComparisonListener(String testName) {
this.testName = testName;
this.haveSeenDifferenceAlready = new AtomicBoolean();
}
@Override
public void comparisonPerformed(Comparison comparison, ComparisonResult outcome) {
if (outcome == ComparisonResult.DIFFERENT) {
logDifference(comparison);
}
}
private void logDifference(Comparison comparison) {
if (haveSeenDifferenceAlready.compareAndSet(false, true)) {
logPreamble();
}
LOG.warn(comparison.toString());
}
private void logPreamble() {
var testNameOrEmpty = nonNull(testName) ? (testName + ": ") : "";
LOG.warn("{}XML differences found:", testNameOrEmpty);
}
}
}
/*
The test fixtures used above in the tests are shown below.
They are directly under src/test/resources.
Also note we use the Fixtures class from kiwi-test.
See here: https://github.com/kiwiproject/kiwi-test
alice-smith-1.xml:
<person>
<first_name>Alice</first_name>
<last_name>Smith</last_name>
<age>42</age>
<country>USA</country>
</person>
alice-smith-2.xml is a copy of alice-smith-1.xml
alice-smith-3.xml:
<person><first_name>Alice</first_name><last_name>Smith</last_name><age>42</age><country>USA</country></person>
alice-jones.xml:
<person>
<first_name>Alice</first_name>
<last_name>Jones</last_name>
<age>29</age>
<country>USA</country>
</person>
Sample output from the shouldUseOpinionatedXmlAssertToHideTheDetails test:
17:14:17.875 [main] WARN org.kiwiproject.test.playground.XmlUnitLogAllDifferencesTest$LoggingComparisonListener - shouldUseOpinionatedXmlAssertToHideTheDetails(TestInfo): XML differences found:
17:14:17.878 [main] WARN org.kiwiproject.test.playground.XmlUnitLogAllDifferencesTest$LoggingComparisonListener - Expected text value 'Jones' but was 'Smith' - comparing <last_name ...>Jones</last_name> at /person[1]/last_name[1]/text()[1] to <last_name ...>Smith</last_name> at /person[1]/last_name[1]/text()[1]
17:14:17.879 [main] WARN org.kiwiproject.test.playground.XmlUnitLogAllDifferencesTest$LoggingComparisonListener - Expected text value '29' but was '42' - comparing <age ...>29</age> at /person[1]/age[1]/text()[1] to <age ...>42</age> at /person[1]/age[1]/text()[1]
org.opentest4j.AssertionFailedError:
Expecting:
<control instance> and <test instance> to be identical
Expected text value 'Jones' but was 'Smith' - comparing <last_name ...>Jones</last_name> at /person[1]/last_name[1]/text()[1] to <last_name ...>Smith</last_name> at /person[1]/last_name[1]/text()[1]
expected:<<last_name>Jones</last_name>> but was:<<last_name>Smith</last_name>>>
Expected :<last_name>Jones</last_name>
Actual :<last_name>Smith</last_name>
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment