-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
bea7802
commit c3403db
Showing
16 changed files
with
851 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
171 changes: 171 additions & 0 deletions
171
src/main/java/org/kiwiproject/test/xmlunit/KiwiXmlAssert.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <a href="https://www.xmlunit.org">XMLUnit</a> to log | ||
* any differences found using a {@link LoggingComparisonListener}. Optionally, you can specify a test | ||
* name that will be included when logging differences. | ||
* <p> | ||
* 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. | ||
* <p> | ||
* 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: | ||
* <pre> | ||
* // inside a test | ||
* assertThatXml(someXml).isIdenticalTo(someOtherXml); | ||
* </pre> | ||
* <p> | ||
* 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. | ||
* <p> | ||
* If needed, the {@link ComparisonController} can be overridden simply by calling | ||
* {@link CompareAssert#withComparisonController(ComparisonController)} | ||
* on the CompareAssert returned by this method. <em>Note, however, that if you use | ||
* a different ComparisonController which stops at the first difference, then | ||
* only that first difference will be logged.</em> | ||
* <p> | ||
* 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(); | ||
} | ||
} |
103 changes: 103 additions & 0 deletions
103
src/main/java/org/kiwiproject/test/xmlunit/LoggingComparisonListener.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
* <p> | ||
* 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); | ||
} | ||
} |
96 changes: 96 additions & 0 deletions
96
src/test/java/org/kiwiproject/test/logback/InMemoryAppender.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
* <p> | ||
* <em>This is for testing purposes only, and is not at all intended for production use!</em> | ||
*/ | ||
public class InMemoryAppender extends AppenderBase<ILoggingEvent> { | ||
|
||
private final AtomicInteger messageOrder; | ||
private final ConcurrentMap<Integer, ILoggingEvent> 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<ILoggingEvent> 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<ILoggingEvent> 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<String> 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<ILoggingEvent> orderedEventStream() { | ||
return eventMap.values() | ||
.stream() | ||
.sorted(comparing(ILoggingEvent::getTimeStamp)); | ||
} | ||
} |
Oops, something went wrong.