From c3403db55ee0e99ea2f88e48a84c78beeb502661 Mon Sep 17 00:00:00 2001 From: Scott Leberknight <174812+sleberknight@users.noreply.github.com> Date: Sun, 10 Dec 2023 14:42:39 -0500 Subject: [PATCH] Add XMLUnit test utils: KiwiXmlAssert and LoggingComparisonListener (#443) * KiwiXmlAssert is a simple wrapper around XMLUnit's XmlAssert which logs all differences. It also provides some convenience methods to compare XML without any boilerplate code. * LoggingComparisonListener is an XMLUnit ComparisonListener that logs differences found by XmlAssert. * Both of these classes are marked Beta in this first iteration * Additionally, added InMemoryAppender and InMemoryAppenderExtension, which provide test utilities for testing Logback messages. They are only in the test sources for now, but I documented them fully because I anticipate we might want to add them as full test utilities at some point. And if not, then at least they are documented for internal use within this library. Closes #442 --- pom.xml | 8 + .../test/xmlunit/KiwiXmlAssert.java | 171 +++++++++++++++ .../xmlunit/LoggingComparisonListener.java | 103 +++++++++ .../test/logback/InMemoryAppender.java | 96 +++++++++ .../logback/InMemoryAppenderExtension.java | 106 ++++++++++ .../test/xmlunit/KiwiXmlAssertTest.java | 197 ++++++++++++++++++ .../LoggingComparisonListenerTest.java | 86 ++++++++ .../KiwiXmlAssertTest/alice-jones.xml | 6 + .../alice-smith-condensed.xml | 6 + .../alice-smith-duplicate.xml | 6 + .../alice-smith-extra-whitespace.xml | 19 ++ ...ith-with-comments-and-extra-whitespace.xml | 14 ++ ...alice-smith-with-comments-between-tags.xml | 7 + .../alice-smith-with-comments.xml | 12 ++ .../KiwiXmlAssertTest/alice-smith.xml | 6 + src/test/resources/logback-test.xml | 8 + 16 files changed, 851 insertions(+) create mode 100644 src/main/java/org/kiwiproject/test/xmlunit/KiwiXmlAssert.java create mode 100644 src/main/java/org/kiwiproject/test/xmlunit/LoggingComparisonListener.java create mode 100644 src/test/java/org/kiwiproject/test/logback/InMemoryAppender.java create mode 100644 src/test/java/org/kiwiproject/test/logback/InMemoryAppenderExtension.java create mode 100644 src/test/java/org/kiwiproject/test/xmlunit/KiwiXmlAssertTest.java create mode 100644 src/test/java/org/kiwiproject/test/xmlunit/LoggingComparisonListenerTest.java create mode 100644 src/test/resources/KiwiXmlAssertTest/alice-jones.xml create mode 100644 src/test/resources/KiwiXmlAssertTest/alice-smith-condensed.xml create mode 100644 src/test/resources/KiwiXmlAssertTest/alice-smith-duplicate.xml create mode 100644 src/test/resources/KiwiXmlAssertTest/alice-smith-extra-whitespace.xml create mode 100644 src/test/resources/KiwiXmlAssertTest/alice-smith-with-comments-and-extra-whitespace.xml create mode 100644 src/test/resources/KiwiXmlAssertTest/alice-smith-with-comments-between-tags.xml create mode 100644 src/test/resources/KiwiXmlAssertTest/alice-smith-with-comments.xml create mode 100644 src/test/resources/KiwiXmlAssertTest/alice-smith.xml diff --git a/pom.xml b/pom.xml index 27fedfea..ede1fc9c 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,7 @@ 3.1.0 + 2.9.1 kiwiproject_kiwi-test @@ -231,6 +232,13 @@ provided + + org.xmlunit + xmlunit-assertj + ${xmlunit.version} + provided + + diff --git a/src/main/java/org/kiwiproject/test/xmlunit/KiwiXmlAssert.java b/src/main/java/org/kiwiproject/test/xmlunit/KiwiXmlAssert.java new file mode 100644 index 00000000..4c450822 --- /dev/null +++ b/src/main/java/org/kiwiproject/test/xmlunit/KiwiXmlAssert.java @@ -0,0 +1,171 @@ +package org.kiwiproject.test.xmlunit; + +import static org.kiwiproject.base.KiwiPreconditions.requireNotNull; + +import com.google.common.annotations.Beta; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import org.junit.jupiter.api.TestInfo; +import org.xmlunit.assertj.CompareAssert; +import org.xmlunit.assertj.XmlAssert; +import org.xmlunit.diff.ComparisonController; +import org.xmlunit.diff.ComparisonControllers; +import org.xmlunit.diff.ComparisonListener; + +/** + * KiwiXmlAssert provides convenience on top of XMLUnit to log + * any differences found using a {@link LoggingComparisonListener}. Optionally, you can specify a test + * name that will be included when logging differences. + *

+ * Once the terminal {@link #and(Object)} method is called, you use XMLUnit as usual. + * Or, you can use one of the convenience methods such as {@link #isIdenticalTo(Object)} + * which are useful for some common comparison scenarios. + */ +@Beta +public class KiwiXmlAssert { + + private final Object o; + private String testName; + + private KiwiXmlAssert(Object o) { + this.o = requireNotNull(o, "object to compare against must not bul null"); + } + + /** + * This is an entrance point to begin an XML comparison. + *

+ * Returns a KiwiXmlAssert object for making assertions on the given object. + * + * @param o object with a type supported by {@link org.xmlunit.builder.Input#from(Object)} + * @return a new KiwiXmlAssert object + */ + public static KiwiXmlAssert assertThat(Object o) { + return new KiwiXmlAssert(o); + } + + /** + * This is an alternate entrance point to begin an XML comparison. It is useful in + * tests which already statically import AssertJ's {@code assertThat}, because + * it avoids ambiguity and compilation errors while allowing it to be statically + * imported. For example, assuming this method has been statically imported: + *

+     * // inside a test
+     * assertThatXml(someXml).isIdenticalTo(someOtherXml);
+     * 
+ *

+ * Returns a KiwiXmlAssert object for making assertions on the given object. + * + * @param o object with a type supported by {@link org.xmlunit.builder.Input#from(Object)} + * @return a new KiwiXmlAssert object + */ + public static KiwiXmlAssert assertThatXml(Object o) { + return assertThat(o); + } + + /** + * Sets the test name for logging XML differences during comparison. + * + * @param testInfo the TestInfo object containing information about the current test + * @return this KiwiXmlAssert object updated with the updated test name + */ + public KiwiXmlAssert withTestNameFrom(TestInfo testInfo) { + return withTestName(requireNotNull(testInfo).getDisplayName()); + } + + /** + * Sets the test name for logging XML differences during comparison. + * + * @param testName the name of the test + * @return this KiwiXmlAssert object updated with the modified test name + */ + public KiwiXmlAssert withTestName(String testName) { + this.testName = testName; + return this; + } + + /** + * This is a terminal operation in {@link KiwiXmlAssert}. It returns a {@link CompareAssert} + * that has been configured with a {@link LoggingComparisonListener} and the + * {@link ComparisonControllers#Default Default} comparison controller. + *

+ * If needed, the {@link ComparisonController} can be overridden simply by calling + * {@link CompareAssert#withComparisonController(ComparisonController)} + * on the CompareAssert returned by this method. Note, however, that if you use + * a different ComparisonController which stops at the first difference, then + * only that first difference will be logged. + *

+ * Additional {@link org.xmlunit.diff.ComparisonListener comparison listeners} can be + * added by calling {@link CompareAssert#withComparisonListeners(ComparisonListener...)} + * and supplying additional listeners. + * + * @param control the object to compare against this instance's object + * @return a CompareAssert object for making further assertions on XML differences + * @see CompareAssert + * @see ComparisonControllers#Default + */ + public CompareAssert and(Object control) { + return XmlAssert.assertThat(o) + .and(control) + .withComparisonController(ComparisonControllers.Default) + .withComparisonListeners(new LoggingComparisonListener(testName)); + } + + /** + * This is a convenience terminal operation in {@link KiwiXmlAssert} built using + * {@link #and(Object)} that checks that the control object is identical to this + * instance's object. + * + * @param control the object to compare against this instance's object + * @return a CompareAssert object for making further assertions on XML differences + * @see CompareAssert#areIdentical() + */ + @CanIgnoreReturnValue + public CompareAssert isIdenticalTo(Object control) { + return and(control).areIdentical(); + } + + /** + * This is a convenience terminal operation in {@link KiwiXmlAssert} built using + * {@link #and(Object)} that checks that the control object is identical to this + * instance's object, ignoring extra whitespace. + * + * @param control the object to compare against this instance's object + * @return a CompareAssert object for making further assertions on XML differences + * @see CompareAssert#ignoreWhitespace() + * @see CompareAssert#areIdentical() + */ + @CanIgnoreReturnValue + public CompareAssert isIdenticalToIgnoringWhitespace(Object control) { + return and(control).ignoreWhitespace().areIdentical(); + } + + /** + * This is a convenience terminal operation in {@link KiwiXmlAssert} built using + * {@link #and(Object)} that checks that the control object is identical to this + * instance's object, ignoring XML comments. + * + * @param control the object to compare against this instance's object + * @return a CompareAssert object for making further assertions on XML differences + * @see CompareAssert#ignoreComments() + * @see CompareAssert#areIdentical() + */ + @CanIgnoreReturnValue + public CompareAssert isIdenticalToIgnoringComments(Object control) { + return and(control).ignoreComments().areIdentical(); + } + + /** + * This is a convenience terminal operation in {@link KiwiXmlAssert} built using + * {@link #and(Object)} that checks that the control object is identical to this + * instance's object, ignoring both whitespace and comments. + * + * @param control the object to compare against this instance's object + * @return a CompareAssert object for making further assertions on XML differences + * @see CompareAssert#ignoreWhitespace() + * @see CompareAssert#ignoreComments() + * @see CompareAssert#areIdentical() + */ + @CanIgnoreReturnValue + public CompareAssert isIdenticalToIgnoringWhitespaceAndComments(Object control) { + return and(control).ignoreWhitespace().ignoreComments().areIdentical(); + } +} diff --git a/src/main/java/org/kiwiproject/test/xmlunit/LoggingComparisonListener.java b/src/main/java/org/kiwiproject/test/xmlunit/LoggingComparisonListener.java new file mode 100644 index 00000000..7477db6d --- /dev/null +++ b/src/main/java/org/kiwiproject/test/xmlunit/LoggingComparisonListener.java @@ -0,0 +1,103 @@ +package org.kiwiproject.test.xmlunit; + +import static java.util.Objects.nonNull; +import static org.kiwiproject.base.KiwiPreconditions.requireNotNull; + +import com.google.common.annotations.Beta; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.junit.jupiter.api.TestInfo; +import org.slf4j.Logger; +import org.xmlunit.diff.Comparison; +import org.xmlunit.diff.ComparisonListener; +import org.xmlunit.diff.ComparisonResult; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A listener for XML comparison events that logs any differences using a logger. + * + * @see ComparisonListener + */ +@Slf4j +@Beta +public class LoggingComparisonListener implements ComparisonListener { + + private final String testName; + private final AtomicBoolean haveSeenDifferenceAlready; + + /** + * Private {@link Logger} used for logging XML differences. By default, it is + * the {@code static final} Logger for this class, but can be changed using the + * {@link #withLogger(Logger)} method. + */ + @SuppressWarnings("NonConstantLogger") + private Logger logger = LOG; + + /** + * Create a new instance without an explicit test name. + */ + public LoggingComparisonListener() { + this((String) null); + } + + /** + * Create a new instance with a test name extracted from the given TestInfo. + * + * @param testInfo the JUnit {@link TestInfo} + */ + public LoggingComparisonListener(TestInfo testInfo) { + this(requireNotNull(testInfo).getDisplayName()); + } + + /** + * Create a new instance with the given test name. + * + * @param testName the test name + */ + public LoggingComparisonListener(@Nullable String testName) { + this.testName = testName; + this.haveSeenDifferenceAlready = new AtomicBoolean(); + } + + /** + * Sets the logger to be used for logging XML differences. + *

+ * If this is not called, a default logger is used. + * + * @param logger the logger to set + * @return the current instance of {@link LoggingComparisonListener} + */ + public LoggingComparisonListener withLogger(Logger logger) { + this.logger = requireNotNull(logger); + return this; + } + + /** + * This method is called when a comparison is performed. If the outcome of the comparison is DIFFERENT, + * it logs the difference, including a test name if it was provided to this instance. + * + * @param comparison the comparison object + * @param outcome the outcome of the comparison + */ + @Override + public void comparisonPerformed(Comparison comparison, ComparisonResult outcome) { + if (outcome == ComparisonResult.DIFFERENT) { + logDifference(comparison); + } + } + + @SuppressWarnings("java:S2629") + private void logDifference(Comparison comparison) { + if (haveSeenDifferenceAlready.compareAndSet(false, true)) { + logPreamble(); + } + + logger.warn(comparison.toString()); + } + + private void logPreamble() { + var testNameOrEmpty = nonNull(testName) ? (testName + ": ") : ""; + logger.warn("{}XML differences found:", testNameOrEmpty); + } +} diff --git a/src/test/java/org/kiwiproject/test/logback/InMemoryAppender.java b/src/test/java/org/kiwiproject/test/logback/InMemoryAppender.java new file mode 100644 index 00000000..19531580 --- /dev/null +++ b/src/test/java/org/kiwiproject/test/logback/InMemoryAppender.java @@ -0,0 +1,96 @@ +package org.kiwiproject.test.logback; + +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import lombok.Synchronized; + +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +/** + * A logback appender that stores logging events in an in-memory map. + * The events can be accessed, ordered, and cleared. + *

+ * This is for testing purposes only, and is not at all intended for production use! + */ +public class InMemoryAppender extends AppenderBase { + + private final AtomicInteger messageOrder; + private final ConcurrentMap eventMap; + + /** + * Create a new InMemoryAppender with no messages. + */ + public InMemoryAppender() { + this.messageOrder = new AtomicInteger(); + this.eventMap = new ConcurrentHashMap<>(); + } + + /** + * Assert this appender has the expected number of logging events, and if the assertion succeeds, return a + * list containing those events. + */ + @SuppressWarnings("unused") + public List assertNumberOfLoggingEventsAndGet(int expectedEventCount) { + var events = orderedEvents(); + assertThat(events).hasSize(expectedEventCount); + return events; + } + + /** + * {@inheritDoc} + */ + @Override + @Synchronized + protected void append(ILoggingEvent eventObject) { + eventMap.put(messageOrder.incrementAndGet(), eventObject); + } + + /** + * Clears all logging events from this appender. + */ + @Synchronized + public void clearEvents() { + messageOrder.set(0); + eventMap.clear(); + } + + /** + * Retrieves a list of logging events ordered by ascending timestamp. + * + * @return the ordered list of logging events + */ + public List orderedEvents() { + return orderedEventStream().collect(toList()); + } + + /** + * Retrieves a list of logged messages ordered by ascending timestamp. + * + * @return the ordered list of logged messages + */ + @SuppressWarnings("unused") + public List orderedEventMessages() { + return orderedEventStream() + .map(ILoggingEvent::getFormattedMessage) + .collect(toList()); + } + + /** + * Retrieves a stream of logging events ordered by ascending timestamp. + * + * @return the ordered stream of logged messages + */ + public Stream orderedEventStream() { + return eventMap.values() + .stream() + .sorted(comparing(ILoggingEvent::getTimeStamp)); + } +} diff --git a/src/test/java/org/kiwiproject/test/logback/InMemoryAppenderExtension.java b/src/test/java/org/kiwiproject/test/logback/InMemoryAppenderExtension.java new file mode 100644 index 00000000..049039ef --- /dev/null +++ b/src/test/java/org/kiwiproject/test/logback/InMemoryAppenderExtension.java @@ -0,0 +1,106 @@ +package org.kiwiproject.test.logback; + +import static org.assertj.core.api.Assertions.assertThat; + +import ch.qos.logback.classic.Logger; +import lombok.Getter; +import lombok.experimental.Accessors; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.slf4j.LoggerFactory; + + +/** + * A JUnit 5 extension that allows testing messages logged using Logback. + * Uses {@link InMemoryAppender} to store logged messages, so that tests + * can retrieve and verify them later. + */ +public class InMemoryAppenderExtension implements BeforeEachCallback, AfterEachCallback { + + private final Class loggerClass; + private final String appenderName; + + @Getter + @Accessors(fluent = true) + private InMemoryAppender appender; + + /** + * Create a new instance associated with the given Logback logger class. + * The appender name must be the simple name of the logger class + * suffixed with "Appender". So, if the logger class is + * {@code com.acme.space.modulator.SpaceModulatorServiceTest.class}, then the + * appender must be named {@code SpaceModulatorServiceTestAppender}. + *

+ * Note also the appender must be an {@link InMemoryAppender}. + * + * @param loggerClass the class of the test logger + */ + public InMemoryAppenderExtension(Class loggerClass) { + this(loggerClass, loggerClass.getSimpleName() + "Appender"); + } + + /** + * Create a new instance associated with the given Logback logger class + * which has an appender of type {@link InMemoryAppender} with the name + * {@code appenderName}. + * + * @param loggerClass the class of the test logger + * @param appenderName the name of the {@link InMemoryAppender} + */ + public InMemoryAppenderExtension(Class loggerClass, String appenderName) { + this.loggerClass = loggerClass; + this.appenderName = appenderName; + } + + /** + * Exposes the {@link InMemoryAppender} associated with {@code loggerClass}. + * It can be obtained via the {@link #appender()} method. Usually, tests + * will store an instance in their own {@link org.junit.jupiter.api.BeforeEach BeforeEach} + * method. For example: + *

+     * class SpaceModulatorTest {
+     *
+     *     {@literal @}RegisterExtension
+     *      private final InMemoryAppenderExtension inMemoryAppenderExtension =
+     *                 new InMemoryAppenderExtension(SpaceModulatorServiceTest.class);
+     *
+     *      private InMemoryAppender appender;
+     *
+     *     {@literal @}BeforeEach
+     *      void setUp() {
+     *          this.appender = inMemoryAppenderExtension.appender();
+     *
+     *          // additional set up...
+     *      }
+     *
+     *      // tests...
+     *  }
+     * 
+ * + * @param context the current extension context; never {@code null} + */ + @Override + public void beforeEach(ExtensionContext context) { + var logbackLogger = (Logger) LoggerFactory.getLogger(loggerClass); + var rawAppender = logbackLogger.getAppender(appenderName); + + assertThat(rawAppender) + .describedAs("Expected an appender named '%s' for logger '%s' of type %s", + appenderName, loggerClass.getName(), InMemoryAppender.class.getName()) + .isInstanceOf(InMemoryAppender.class); + appender = (InMemoryAppender) rawAppender; + } + + /** + * Clears all logging events from the {@link InMemoryAppender} to ensure each + * test starts with an empty appender. + * + * @param context the current extension context; never {@code null} + * @see InMemoryAppender#clearEvents() + */ + @Override + public void afterEach(ExtensionContext context) { + appender.clearEvents(); + } +} diff --git a/src/test/java/org/kiwiproject/test/xmlunit/KiwiXmlAssertTest.java b/src/test/java/org/kiwiproject/test/xmlunit/KiwiXmlAssertTest.java new file mode 100644 index 00000000..5501593d --- /dev/null +++ b/src/test/java/org/kiwiproject/test/xmlunit/KiwiXmlAssertTest.java @@ -0,0 +1,197 @@ +package org.kiwiproject.test.xmlunit; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.kiwiproject.test.util.Fixtures.fixture; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@DisplayName("KiwiXmlAssert") +@SuppressWarnings("java:S5778") +class KiwiXmlAssertTest { + + @Nested + class EntrancePoints { + + @Test + void canUseAssertThatXml() { + var xml = fixture("KiwiXmlAssertTest/alice-smith.xml"); + var otherXml = fixture("KiwiXmlAssertTest/alice-smith-duplicate.xml"); + + assertThatCode(() -> KiwiXmlAssert.assertThatXml(xml).isIdenticalTo(otherXml)) + .doesNotThrowAnyException(); + } + + @Test + void canUseAssertThat() { + var xml = fixture("KiwiXmlAssertTest/alice-smith.xml"); + var otherXml = fixture("KiwiXmlAssertTest/alice-smith-duplicate.xml"); + + assertThatCode(() -> KiwiXmlAssert.assertThat(xml).isIdenticalTo(otherXml)) + .doesNotThrowAnyException(); + } + } + + @Nested + class CanUseTestName { + + @Test + void shouldAcceptTestNameAsString() { + var xml = fixture("KiwiXmlAssertTest/alice-smith.xml"); + var otherXml = fixture("KiwiXmlAssertTest/alice-jones.xml"); + + assertThatThrownBy(() -> + KiwiXmlAssert.assertThat(xml) + .withTestName("custom-test-name") + .and(otherXml) + .areIdentical()) + .isExactlyInstanceOf(AssertionError.class); + } + + @Test + void shouldAcceptTestNameFromTestInfo(TestInfo testInfo) { + var xml = fixture("KiwiXmlAssertTest/alice-smith.xml"); + var otherXml = fixture("KiwiXmlAssertTest/alice-jones.xml"); + + assertThatThrownBy(() -> + KiwiXmlAssert.assertThat(xml) + .withTestNameFrom(testInfo) + .and(otherXml) + .areIdentical()) + .isExactlyInstanceOf(AssertionError.class); + } + } + + @Nested + class TerminalOperations { + + @Nested + class And { + + @Test + void shouldReturnCompareAssertForFurtherChaining() { + var xml = fixture("KiwiXmlAssertTest/alice-smith.xml"); + var otherXml = fixture("KiwiXmlAssertTest/alice-smith-duplicate.xml"); + + assertThatCode(() -> KiwiXmlAssert.assertThat(xml) + .withTestName("custom-test-name") + .and(otherXml) + .ignoreWhitespace() + .ignoreChildNodesOrder() + .ignoreComments() + .areIdentical()) + .doesNotThrowAnyException(); + } + } + + @Nested + class IsIdenticalTo { + + @Test + void shouldCompareIdenticalXml() { + var xml = fixture("KiwiXmlAssertTest/alice-smith.xml"); + var otherXml = fixture("KiwiXmlAssertTest/alice-smith-duplicate.xml"); + + assertThatCode(() -> KiwiXmlAssert.assertThat(xml).isIdenticalTo(otherXml)) + .doesNotThrowAnyException(); + } + + @Test + void shouldThrowAssertionErrorWhenXmlIsDifferent() { + var xml = fixture("KiwiXmlAssertTest/alice-smith.xml"); + var otherXml = fixture("KiwiXmlAssertTest/alice-jones.xml"); + + assertThatThrownBy(() -> + KiwiXmlAssert.assertThat(xml).isIdenticalTo(otherXml)) + .isExactlyInstanceOf(AssertionError.class); + } + } + + @Nested + class IsIdenticalToIgnoringWhitespace { + + @ParameterizedTest + @ValueSource(strings = { + "KiwiXmlAssertTest/alice-smith-condensed.xml", + "KiwiXmlAssertTest/alice-smith-extra-whitespace.xml" + }) + void shouldIgnoreWhitespace(String otherFixture) { + var xml = fixture("KiwiXmlAssertTest/alice-smith.xml"); + var otherXml = fixture(otherFixture); + + assertThatCode(() -> + KiwiXmlAssert.assertThat(xml).isIdenticalToIgnoringWhitespace(otherXml)) + .doesNotThrowAnyException(); + } + + @Test + void shouldThrowAssertionErrorWhenXmlIsDifferent() { + var xml = fixture("KiwiXmlAssertTest/alice-smith.xml"); + var otherXml = fixture("KiwiXmlAssertTest/alice-jones.xml"); + + assertThatThrownBy(() -> + KiwiXmlAssert.assertThat(xml).isIdenticalToIgnoringWhitespace(otherXml)) + .isExactlyInstanceOf(AssertionError.class); + } + } + + @Nested + class IsIdenticalToIgnoringComments { + + @Test + void shouldIgnoreComments() { + var xml = fixture("KiwiXmlAssertTest/alice-smith.xml"); + var otherXml = fixture("KiwiXmlAssertTest/alice-smith-with-comments.xml"); + + assertThatCode(() -> + KiwiXmlAssert.assertThat(xml).isIdenticalToIgnoringComments(otherXml)) + .doesNotThrowAnyException(); + } + + /** + * @implNote This is a "canary test" to flag if XMLUnit ever changes the unexpected + * behavior that it doesn't strip comments that are between tags, only comments + * at the top and bottom, and within elements. View the test file to see. + */ + @Test + void canaryDoesNotIgnoreCommentsBetweenTags() { + var xml = fixture("KiwiXmlAssertTest/alice-smith.xml"); + var otherXml = fixture("KiwiXmlAssertTest/alice-smith-with-comments-between-tags.xml"); + + assertThatThrownBy(() -> + KiwiXmlAssert.assertThat(xml).isIdenticalToIgnoringComments(otherXml)) + .isExactlyInstanceOf(AssertionError.class); + } + } + + @Nested + class IsIdenticalToIgnoringWhitespaceAndComments { + + @Test + void shouldIgnoreWhitespaceAndComments() { + var xml = fixture("KiwiXmlAssertTest/alice-smith.xml"); + var otherXml = fixture("KiwiXmlAssertTest/alice-smith-with-comments-and-extra-whitespace.xml"); + + assertThatCode(() -> + KiwiXmlAssert.assertThat(xml).isIdenticalToIgnoringWhitespaceAndComments(otherXml)) + .doesNotThrowAnyException(); + } + + @Test + void shouldThrowAssertionErrorWhenXmlIsDifferent() { + var xml = fixture("KiwiXmlAssertTest/alice-smith.xml"); + var otherXml = fixture("KiwiXmlAssertTest/alice-jones.xml"); + + assertThatThrownBy(() -> + KiwiXmlAssert.assertThat(xml).isIdenticalToIgnoringWhitespaceAndComments(otherXml)) + .isExactlyInstanceOf(AssertionError.class); + } + } + } + +} diff --git a/src/test/java/org/kiwiproject/test/xmlunit/LoggingComparisonListenerTest.java b/src/test/java/org/kiwiproject/test/xmlunit/LoggingComparisonListenerTest.java new file mode 100644 index 00000000..6f47f771 --- /dev/null +++ b/src/test/java/org/kiwiproject/test/xmlunit/LoggingComparisonListenerTest.java @@ -0,0 +1,86 @@ +package org.kiwiproject.test.xmlunit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.kiwiproject.test.logback.InMemoryAppender; +import org.kiwiproject.test.logback.InMemoryAppenderExtension; +import org.xmlunit.diff.Comparison; +import org.xmlunit.diff.ComparisonResult; + +@DisplayName("LoggingComparisonListener") +@Slf4j +class LoggingComparisonListenerTest { + + @RegisterExtension + private final InMemoryAppenderExtension inMemoryAppenderExtension = + new InMemoryAppenderExtension(LoggingComparisonListenerTest.class); + + private InMemoryAppender appender; + private Comparison comparison; + + @BeforeEach + void setUp() { + appender = inMemoryAppenderExtension.appender(); + comparison = mock(Comparison.class); + } + + @ParameterizedTest + @EnumSource(value = ComparisonResult.class, + mode = EnumSource.Mode.EXCLUDE, + names = "DIFFERENT") + void shouldNotLogWhenComparisonResultIsNotDifferent(ComparisonResult result) { + var listener = new LoggingComparisonListener().withLogger(LOG); + listener.comparisonPerformed(comparison, result); + + assertThat(appender.orderedEvents()).isEmpty(); + } + + @Test + void shouldLogWithNoTestName() { + var listener = new LoggingComparisonListener().withLogger(LOG); + + when(comparison.toString()).thenReturn("The difference"); + + listener.comparisonPerformed(comparison, ComparisonResult.DIFFERENT); + + assertThat(appender.orderedEventMessages()) + .containsExactly("XML differences found:", "The difference"); + } + + @Test + void shouldLogWithTestName_FromTestInfo() { + var testInfo = mock(TestInfo.class); + when(testInfo.getDisplayName()).thenReturn("My Test"); + + var listener = new LoggingComparisonListener(testInfo).withLogger(LOG); + + when(comparison.toString()).thenReturn("The difference"); + + listener.comparisonPerformed(comparison, ComparisonResult.DIFFERENT); + + assertThat(appender.orderedEventMessages()) + .containsExactly("My Test: XML differences found:", "The difference"); + } + + @Test + void shouldLogWithTestName() { + var listener = new LoggingComparisonListener("Some Test").withLogger(LOG); + + when(comparison.toString()).thenReturn("The difference"); + + listener.comparisonPerformed(comparison, ComparisonResult.DIFFERENT); + + assertThat(appender.orderedEventMessages()) + .containsExactly("Some Test: XML differences found:", "The difference"); + } +} diff --git a/src/test/resources/KiwiXmlAssertTest/alice-jones.xml b/src/test/resources/KiwiXmlAssertTest/alice-jones.xml new file mode 100644 index 00000000..6268ea54 --- /dev/null +++ b/src/test/resources/KiwiXmlAssertTest/alice-jones.xml @@ -0,0 +1,6 @@ + + Alice + Jones + 29 + USA + diff --git a/src/test/resources/KiwiXmlAssertTest/alice-smith-condensed.xml b/src/test/resources/KiwiXmlAssertTest/alice-smith-condensed.xml new file mode 100644 index 00000000..4fa4e54a --- /dev/null +++ b/src/test/resources/KiwiXmlAssertTest/alice-smith-condensed.xml @@ -0,0 +1,6 @@ + + Alice + Smith + 42 + USA + diff --git a/src/test/resources/KiwiXmlAssertTest/alice-smith-duplicate.xml b/src/test/resources/KiwiXmlAssertTest/alice-smith-duplicate.xml new file mode 100644 index 00000000..4fa4e54a --- /dev/null +++ b/src/test/resources/KiwiXmlAssertTest/alice-smith-duplicate.xml @@ -0,0 +1,6 @@ + + Alice + Smith + 42 + USA + diff --git a/src/test/resources/KiwiXmlAssertTest/alice-smith-extra-whitespace.xml b/src/test/resources/KiwiXmlAssertTest/alice-smith-extra-whitespace.xml new file mode 100644 index 00000000..0a0720a6 --- /dev/null +++ b/src/test/resources/KiwiXmlAssertTest/alice-smith-extra-whitespace.xml @@ -0,0 +1,19 @@ + + + + Alice + + + + Smith + + + + 42 + + + + USA + + + diff --git a/src/test/resources/KiwiXmlAssertTest/alice-smith-with-comments-and-extra-whitespace.xml b/src/test/resources/KiwiXmlAssertTest/alice-smith-with-comments-and-extra-whitespace.xml new file mode 100644 index 00000000..69582dd7 --- /dev/null +++ b/src/test/resources/KiwiXmlAssertTest/alice-smith-with-comments-and-extra-whitespace.xml @@ -0,0 +1,14 @@ + + + + Alice + + Smith + + 42 + + USA + + + + diff --git a/src/test/resources/KiwiXmlAssertTest/alice-smith-with-comments-between-tags.xml b/src/test/resources/KiwiXmlAssertTest/alice-smith-with-comments-between-tags.xml new file mode 100644 index 00000000..0bd095e3 --- /dev/null +++ b/src/test/resources/KiwiXmlAssertTest/alice-smith-with-comments-between-tags.xml @@ -0,0 +1,7 @@ + + Alice + Smith + 42 + + USA + diff --git a/src/test/resources/KiwiXmlAssertTest/alice-smith-with-comments.xml b/src/test/resources/KiwiXmlAssertTest/alice-smith-with-comments.xml new file mode 100644 index 00000000..3a33afd6 --- /dev/null +++ b/src/test/resources/KiwiXmlAssertTest/alice-smith-with-comments.xml @@ -0,0 +1,12 @@ + + + + Alice + Smith + 42 + USA + + diff --git a/src/test/resources/KiwiXmlAssertTest/alice-smith.xml b/src/test/resources/KiwiXmlAssertTest/alice-smith.xml new file mode 100644 index 00000000..4fa4e54a --- /dev/null +++ b/src/test/resources/KiwiXmlAssertTest/alice-smith.xml @@ -0,0 +1,6 @@ + + Alice + Smith + 42 + USA + diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index ee860bf2..49d1517e 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -10,6 +10,14 @@ + + + + + + + +