Handler vs Matcher

A lot of times you need to check if two things are equal. Instead of creating multiple matchers like arraysEqual , pathsEqual , myDataEqual , you define additional equality rule.Adding a new equality and greater/less rules is done by creating and registering new .

CompareToHandler

To add a new handler you need toimplement register new class using Java Services Interface public interface CompareToHandler { /** * determines whether supports equality comparison * @param actual actual value * @param expected expected value * @return true if comparison can be handled */ boolean handleEquality(Object actual, Object expected); /** * value optionally can be converted to another value to be passed down comparison chain. * exposed as outside method for more precise reporting of actual values in case of a failure. * * @param actual original actual * @param expected expected value * @return optionally converted actual */ default Object convertedActual(Object actual, Object expected) { return actual; } /** * value optionally can be converted to another value to be passed down comparison chain. * exposed as outside method for more precise reporting of expected values for reporting * * @param actual original actual * @param expected original expected * @return optionally converted expected */ default Object convertedExpected(Object actual, Object expected) { return expected; } /** * determines whether supports greater/less than comparison family * @param actual actual value * @param expected expected value * @return true if comparison can be handled */ default boolean handleGreaterLessEqual(Object actual, Object expected) { return false; } /** * determines whether handler can handle nulls. usually left unimplemented * @return true if handler can match nulls */ default boolean handleNulls() { return false; } /** * implementation logic of equality only * @param comparator comparator to delegate comparison to * @param actualPath path to use for reporting * @param actual actual value * @param expected expected value */ void compareEqualOnly(CompareToComparator comparator, ValuePath actualPath, Object actual, Object expected); /** * implementation logic of greater/less than family * @param comparator comparator to delegate comparison to * @param actualPath path to use for reporting * @param actual actual value * @param expected expected value */ default void compareGreaterLessEqual(CompareToComparator comparator, ValuePath actualPath, Object actual, Object expected) { throw new UnsupportedOperationException("greater-less comparison is not implemented"); } } Example Implementation Below is an existing implementation of handler to deal with Java Beans and Java Records as actual and Map as expected public class MapAndJavaBeanOrRecordCompareToHandler extends MapAsExpectedCompareToHandlerBase { @Override protected boolean handleEquality(Object actual) { return !(actual instanceof Iterable || actual instanceof Map); } @Override public Object convertedActual(Object actual, Object expected) { if (actual instanceof ObjectProperties) { return ((ObjectProperties) actual).getTopLevelProperties(); } if (JavaRecordUtils.isRecord(actual)) { return JavaRecordUtils.convertRecordToMap(actual); } return JavaBeanUtils.convertBeanToMap(actual); } } Registration WebTau uses https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html Service Loaders to discover implementations of handlers. To register an additional handler you need to create a file under resources META-INF/services/org.testingisdocumenting.webtau.expectation.equality.CompareToHandler with the content file containing a line per implementing class org.testingisdocumenting.webtau.expectation.equality.handlers.MapAndJavaBeanOrRecordCompareToHandler

Custom Complex Domain Data

Another example to use custom handler is to provide better mismatch details. Imagine you have some CustomComplexData class that contains important data from you domain.This class also has equals defined, and you can use it to check if two values are the same. But when it comes to use it during testing, you find that failure report is hard to comprehend: default assertion only prints toString representation leaving you to eyeball the rest. CustomComplexData calculated = new CustomComplexData("cA", "cB"); calculated.addRow(1, 2); calculated.addRow(3, 4); CustomComplexData expected = new CustomComplexData("cA", "cB"); expected.addRow(1, 2); expected.addRow(3, 5); Assert.assertEquals(expected, calculated); expected:<CustomComplexData{columnNames=[cA, cB], values=[[1, 2], [3, 5]]}> but was:<CustomComplexData{columnNames=[cA, cB], values=[[1, 2], [3, 4]]}> To improve the situation, we will define CustomComplexData : package org.example.domain; import org.testingisdocumenting.webtau.data.ValuePath; import org.testingisdocumenting.webtau.data.table.TableData; import org.testingisdocumenting.webtau.data.table.header.TableDataHeader; import org.testingisdocumenting.webtau.expectation.equality.CompareToComparator; import org.testingisdocumenting.webtau.expectation.equality.CompareToHandler; public class CustomComplexDataCompareToHandler implements CompareToHandler { @Override public boolean handleEquality(Object actual, Object expected) { return actual instanceof CustomComplexData && ( // handler for actual as your data expected instanceof CustomComplexData || // and expected as either your data or TableData expected instanceof TableData); } @Override public Object convertedActual(Object actual, Object expected) { return createTableDataFromCustomData((CustomComplexData) actual); } @Override public Object convertedExpected(Object actual, Object expected) { if (expected instanceof TableData) { return expected; } return createTableDataFromCustomData((CustomComplexData) expected); } @Override public void compareEqualOnly(CompareToComparator comparator, ValuePath actualPath, Object actual, Object expected) { comparator.compareUsingEqualOnly(actualPath, actual, expected); // delegate back to WebTau to compare using converted types } private TableData createTableDataFromCustomData(CustomComplexData actual) { TableData tableData = new TableData(new TableDataHeader(actual.getColumnNames().stream())); actual.forEach(tableData::addRow); // create TableData from complex domain data return tableData; } } Once handler is registered, additional information will be printed during mismatch: org.example.domain.CustomComplexDataCompareToHandler CustomComplexData calculated = new CustomComplexData("cA", "cB"); calculated.addRow(1, 2); calculated.addRow(3, 4); CustomComplexData expected = new CustomComplexData("cA", "cB"); expected.addRow(1, 2); expected.addRow(3, 5); actual(calculated).should(equal(expected)); X failed expecting [value] to equal cA │ cB 1 │ 2 3 │ 5: [value][1].cB: actual: 4 <java.lang.Integer> expected: 5 <java.lang.Integer> (0ms) cA │ cB 1 │ 2 3 │ **4**