+ * The expected value, converted to a {@link String}, is the + * {@link java.util.regex.Pattern regular expression}; the actual value, also + * converted to a {@link String}, is the input tested against it. This mirrors + * how {@link IsActualContainingExpectedStringValueComparer} treats the expected + * value as the substring to look for. + * + *
+ * The comparison succeeds only when the pattern matches the entire
+ * actual value, using {@link Matcher#matches()}, consistent with
+ * {@link String#matches(String)}. To match only part of the actual value, make
+ * the pattern permissive at both ends, for example .*[0-9]{4}.*.
+ * A . does not match a line terminator unless the
+ * {@link Pattern#DOTALL DOTALL} flag is set, so prefix such a pattern with
+ * (?s) when the actual value may contain a newline, for example
+ * (?s).*[0-9]{4}.*.
+ *
+ *
+ * Useful for columns whose exact content a test does not control but whose + * format it does, such as database-generated identifiers, UUID columns, or + * timestamps rendered into a text column. + * + *
+ * Special case: if both values are null, they match; if exactly one is null, + * they do not. + * + *
+ * This comparer adds no dependency beyond {@code java.util.regex}, so it is also
+ * available as {@link ValueComparers#regularExpressionValueComparer}.
+ *
+ * @author Jeff Jensen
+ * @since 3.6.0
+ */
+public class RegularExpressionValueComparer extends ValueComparerTemplateBase
+{
+ private final Logger log = LoggerFactory.getLogger(getClass());
+
+ @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 = isMatching(rowNum, columnName, expectedValue,
+ actualValue);
+ }
+
+ return isExpected;
+ }
+
+ /**
+ * Returns whether the regular expression held in the expected value matches
+ * the whole actual value, both converted to strings.
+ *
+ * @param rowNum
+ * The current row number comparing, used only to identify an
+ * invalid pattern.
+ * @param columnName
+ * The name of the current column comparing, used only to identify
+ * an invalid pattern.
+ * @param expectedValue
+ * The expected value, holding the regular expression.
+ * @param actualValue
+ * The actual value tested against the regular expression.
+ * @return true if the regular expression matches the entire
+ * actual value string.
+ * @throws DatabaseUnitException
+ * If either value cannot be converted to a string, or the
+ * expected value is not a valid regular expression.
+ */
+ protected boolean isMatching(final int rowNum, final String columnName,
+ final Object expectedValue, final Object actualValue)
+ throws DatabaseUnitException
+ {
+ final String regex = DataType.asString(expectedValue);
+ final String actualValueString = DataType.asString(actualValue);
+ final Pattern pattern = compilePattern(rowNum, columnName, regex);
+ final Matcher matcher = pattern.matcher(actualValueString);
+ final boolean isMatching = matcher.matches();
+ log.debug("isMatching: regex={}, actualValueString={}, isMatching={}",
+ regex, actualValueString, isMatching);
+
+ return isMatching;
+ }
+
+ /**
+ * Compiles the expected value into a {@link Pattern}, turning an invalid
+ * expression into a {@link DatabaseUnitException} identifying the row and
+ * column, consistent with how
+ * {@link IsActualEqualToExpectedJsonValueComparer} reports an unparseable
+ * expected value.
+ */
+ private Pattern compilePattern(final int rowNum, final String columnName,
+ final String regex) throws DatabaseUnitException
+ {
+ try
+ {
+ return Pattern.compile(regex);
+ } catch (final PatternSyntaxException e)
+ {
+ final String message = String.format(
+ "Unable to compile expected value as a regular expression"
+ + " for column '%s', row %d: %s",
+ columnName, rowNum, regex);
+ throw new DatabaseUnitException(message, e);
+ }
+ }
+
+ @Override
+ protected String getFailPhrase()
+ {
+ return "not matching the regular expression";
+ }
+}
diff --git a/src/main/java/org/dbunit/assertion/comparer/value/ValueComparers.java b/src/main/java/org/dbunit/assertion/comparer/value/ValueComparers.java
index a498b7ead..69e9522a2 100644
--- a/src/main/java/org/dbunit/assertion/comparer/value/ValueComparers.java
+++ b/src/main/java/org/dbunit/assertion/comparer/value/ValueComparers.java
@@ -148,6 +148,16 @@ protected ValueComparers()
public static final ValueComparer isActualContainingExpectedStringValueComparer =
new IsActualContainingExpectedStringValueComparer();
+ /**
+ * Checks whether the actual value matches, in its entirety, the regular
+ * expression given as the expected value.
+ *
+ * @see RegularExpressionValueComparer
+ * @since 3.6.0
+ */
+ public static final ValueComparer regularExpressionValueComparer =
+ new RegularExpressionValueComparer();
+
/**
* Verifies nothing and never fails.
*
diff --git a/src/site/asciidoc/datacomparisons/valuecomparer.adoc b/src/site/asciidoc/datacomparisons/valuecomparer.adoc
index 7a6cea525..a8e047e38 100644
--- a/src/site/asciidoc/datacomparisons/valuecomparer.adoc
+++ b/src/site/asciidoc/datacomparisons/valuecomparer.adoc
@@ -17,6 +17,7 @@ It enables comparisons such as:
* greater-than
* less-than
* contains
+* regular expression match
* complex multi-column-based
* dynamically choosing the ValueComparer based on criteria
@@ -46,6 +47,18 @@ plus pre-configured variances (e.g.
link:/dbunit/apidocs/org/dbunit/assertion/comparer/value/ValueComparers.html#isActualWithinOneMinuteNewerOfExpectedTimestamp[isActualWithinOneMinuteNewerOfExpectedTimestamp]).
Start with these as they provide for most comparison needs.
+For a column whose exact content a test does not control but whose format it
+does - a database-generated id, a UUID, a formatted identifier, a timestamp
+rendered into a text column -
+link:/dbunit/apidocs/org/dbunit/assertion/comparer/value/RegularExpressionValueComparer.html[RegularExpressionValueComparer]
+(also
+link:/dbunit/apidocs/org/dbunit/assertion/comparer/value/ValueComparers.html#regularExpressionValueComparer[ValueComparers.regularExpressionValueComparer])
+takes the expected dataset value as a regular expression and passes when it
+matches the actual value in its entirety, the same rule as
+`java.lang.String.matches`. To match only part of the value, wrap the pattern in
+`.*` on both ends, e.g. `.*[0-9]{4}.*`; a `.` does not cross a line terminator,
+so prefix the pattern with `(?s)` when the actual value may contain a newline.
+
Some implementations are deliberately left off
link:/dbunit/apidocs/org/dbunit/assertion/comparer/value/ValueComparers.html[ValueComparers]
because it eagerly instantiates every instance it declares, and doing so there would force
diff --git a/src/test/java/org/dbunit/assertion/DbUnitValueComparerAssertIT.java b/src/test/java/org/dbunit/assertion/DbUnitValueComparerAssertIT.java
index 2d7cbe6f1..e6efb223e 100644
--- a/src/test/java/org/dbunit/assertion/DbUnitValueComparerAssertIT.java
+++ b/src/test/java/org/dbunit/assertion/DbUnitValueComparerAssertIT.java
@@ -102,6 +102,42 @@ void testAssertWithValueComparer_isActualGreaterThan_failsWhenActualIsSmaller()
.isInstanceOf(DbComparisonFailure.class);
}
+ @Test
+ void testAssertWithValueComparer_regularExpression_passesWhenActualMatchesPattern()
+ throws Exception
+ {
+ final Column[] columns =
+ new Column[]{new Column("ID", DataType.VARCHAR)};
+
+ final DefaultTable expected = new DefaultTable("T", columns);
+ expected.addRow(new Object[]{"[0-9]+"});
+
+ final DefaultTable actual = new DefaultTable("T", columns);
+ actual.addRow(new Object[]{"4071"});
+
+ assertDoesNotThrow(() -> sut.assertWithValueComparer(expected, actual,
+ ValueComparers.regularExpressionValueComparer));
+ }
+
+ @Test
+ void testAssertWithValueComparer_regularExpression_failsWhenActualDoesNotMatchPattern()
+ throws Exception
+ {
+ final Column[] columns =
+ new Column[]{new Column("ID", DataType.VARCHAR)};
+
+ final DefaultTable expected = new DefaultTable("T", columns);
+ expected.addRow(new Object[]{"[0-9]+"});
+
+ final DefaultTable actual = new DefaultTable("T", columns);
+ actual.addRow(new Object[]{"4071-A"});
+
+ assertThatThrownBy(() -> sut.assertWithValueComparer(expected, actual,
+ ValueComparers.regularExpressionValueComparer))
+ .as("A value not matching the pattern should fail the comparison.")
+ .isInstanceOf(DbComparisonFailure.class);
+ }
+
@Test
void testAssertWithValueComparer_isActualNull_passesWhenActualIsNull()
throws Exception
diff --git a/src/test/java/org/dbunit/assertion/comparer/value/RegularExpressionValueComparerTest.java b/src/test/java/org/dbunit/assertion/comparer/value/RegularExpressionValueComparerTest.java
new file mode 100644
index 000000000..7703359b2
--- /dev/null
+++ b/src/test/java/org/dbunit/assertion/comparer/value/RegularExpressionValueComparerTest.java
@@ -0,0 +1,400 @@
+package org.dbunit.assertion.comparer.value;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+import java.util.regex.PatternSyntaxException;
+
+import org.dbunit.DatabaseUnitException;
+import org.dbunit.dataset.ITable;
+import org.dbunit.dataset.datatype.DataType;
+import org.junit.jupiter.api.Test;
+
+class RegularExpressionValueComparerTest
+{
+ private final RegularExpressionValueComparer sut =
+ new RegularExpressionValueComparer();
+
+ private final ITable expectedTable = null;
+ private final ITable actualTable = null;
+ private final int rowNum = 5;
+ private final String columnName = "MY_COLUMN";
+ private final DataType dataType = DataType.VARCHAR;
+
+ private boolean isExpected(final Object expectedValue,
+ final Object actualValue) throws DatabaseUnitException
+ {
+ return sut.isExpected(expectedTable, actualTable, rowNum, columnName,
+ dataType, expectedValue, actualValue);
+ }
+
+ @Test
+ void testIsExpected_AllNull_True() throws DatabaseUnitException
+ {
+ final boolean actual = isExpected(null, null);
+
+ assertThat(actual).as("Both values null should have matched.").isTrue();
+ }
+
+ @Test
+ void testIsExpected_ActualNullExpectedNotNull_False()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected("\\d+", null);
+
+ assertThat(actual)
+ .as("Null actual value should not match a non-null pattern.")
+ .isFalse();
+ }
+
+ @Test
+ void testIsExpected_ActualNotNullExpectedNull_False()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected(null, "123");
+
+ assertThat(actual)
+ .as("Non-null actual value should not match a null pattern.")
+ .isFalse();
+ }
+
+ @Test
+ void testIsExpected_LiteralPatternEqualsWholeActualValue_True()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected("abc", "abc");
+
+ assertThat(actual)
+ .as("Literal pattern equal to the whole actual value should have matched.")
+ .isTrue();
+ }
+
+ @Test
+ void testIsExpected_DigitPatternMatchesAllDigitActualValue_True()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected("\\d+", "12345");
+
+ assertThat(actual)
+ .as("Digit pattern should have matched an all-digit actual value.")
+ .isTrue();
+ }
+
+ @Test
+ void testIsExpected_DigitPatternWithNonDigitInActualValue_False()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected("\\d+", "12a45");
+
+ assertThat(actual).as(
+ "Digit pattern should not have matched an actual value containing a letter.")
+ .isFalse();
+ }
+
+ @Test
+ void testIsExpected_DigitPatternMatchesOnlyLeadingDigitsOfActualValue_False()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected("\\d+", "123abc");
+
+ assertThat(actual).as(
+ "Match is anchored to the whole value, so leading-only digits should not have matched.")
+ .isFalse();
+ }
+
+ @Test
+ void testIsExpected_DigitPatternMatchesOnlyMiddleOfActualValue_False()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected("\\d+", "abc123def");
+
+ assertThat(actual).as(
+ "Match is anchored to the whole value, so a matching substring should not have matched.")
+ .isFalse();
+ }
+
+ @Test
+ void testIsExpected_WildcardWrappedPatternMatchesSubstringWithinWholeValue_True()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected(".*\\d{4}.*", "order-2026-xyz");
+
+ assertThat(actual).as(
+ "Wrapping the pattern in .* should let it match a substring within the whole value.")
+ .isTrue();
+ }
+
+ @Test
+ void testIsExpected_CharacterClassAndQuantifierFormatPattern_True()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected("[A-Z]{3}-\\d{4}", "ABC-1234");
+
+ assertThat(actual)
+ .as("Format pattern should have matched a correctly shaped actual value.")
+ .isTrue();
+ }
+
+ @Test
+ void testIsExpected_FormatPatternWithWrongShapeActualValue_False()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected("[A-Z]{3}-\\d{4}", "AB-1234");
+
+ assertThat(actual)
+ .as("Format pattern should not have matched a wrongly shaped actual value.")
+ .isFalse();
+ }
+
+ @Test
+ void testIsExpected_UuidPatternMatchesUuidActualValue_True()
+ throws DatabaseUnitException
+ {
+ final String uuidPattern =
+ "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
+
+ final boolean actual =
+ isExpected(uuidPattern, "3f2504e0-4f89-41d3-9a0c-0305e82c3301");
+
+ assertThat(actual)
+ .as("UUID pattern should have matched a UUID-shaped actual value.")
+ .isTrue();
+ }
+
+ @Test
+ void testIsExpected_AlternationPatternMatchesOneAlternative_True()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected("cat|dog", "dog");
+
+ assertThat(actual)
+ .as("Alternation pattern should have matched one of its alternatives.")
+ .isTrue();
+ }
+
+ @Test
+ void testIsExpected_AlternationPatternMatchesNoAlternative_False()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected("cat|dog", "bird");
+
+ assertThat(actual).as(
+ "Alternation pattern should not have matched a value that is none of its alternatives.")
+ .isFalse();
+ }
+
+ @Test
+ void testIsExpected_PatternIsCaseSensitiveByDefault_False()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected("abc", "ABC");
+
+ assertThat(actual)
+ .as("Matching should be case-sensitive unless the pattern opts out.")
+ .isFalse();
+ }
+
+ @Test
+ void testIsExpected_PatternWithInlineCaseInsensitiveFlag_True()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected("(?i)abc", "ABC");
+
+ assertThat(actual)
+ .as("The inline (?i) flag should make matching case-insensitive.")
+ .isTrue();
+ }
+
+ @Test
+ void testIsExpected_PatternWithInlineDotallFlagMatchesNewline_True()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected("(?s)a.b", "a\nb");
+
+ assertThat(actual)
+ .as("The inline (?s) flag should let a dot match a newline.")
+ .isTrue();
+ }
+
+ @Test
+ void testIsExpected_DotWithoutDotallFlagDoesNotMatchNewline_False()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected("a.b", "a\nb");
+
+ assertThat(actual)
+ .as("Without the (?s) flag a dot should not match a newline.")
+ .isFalse();
+ }
+
+ @Test
+ void testIsExpected_WildcardWrappedPatternDoesNotCrossNewlineInActualValue_False()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected(".*4071.*", "top-line\n4071");
+
+ assertThat(actual).as(
+ "The documented .* wrapping should not reach across a newline without the (?s) flag.")
+ .isFalse();
+ }
+
+ @Test
+ void testIsExpected_DotallWildcardWrappedPatternCrossesNewlineInActualValue_True()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected("(?s).*4071.*", "top-line\n4071");
+
+ assertThat(actual).as(
+ "Prefixing the .* wrapping with (?s) should reach across a newline, as documented.")
+ .isTrue();
+ }
+
+ @Test
+ void testIsExpected_EscapedDotMatchesLiteralDot_True()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected("a\\.b", "a.b");
+
+ assertThat(actual)
+ .as("An escaped dot in the pattern should match a literal dot.")
+ .isTrue();
+ }
+
+ @Test
+ void testIsExpected_EscapedDotDoesNotMatchOtherCharacter_False()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected("a\\.b", "axb");
+
+ assertThat(actual).as(
+ "An escaped dot in the pattern should not match a non-dot character.")
+ .isFalse();
+ }
+
+ @Test
+ void testIsExpected_ExplicitlyAnchoredPattern_True()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected("^\\d+$", "123");
+
+ assertThat(actual).as(
+ "Redundant explicit anchors should not prevent a whole-value match.")
+ .isTrue();
+ }
+
+ @Test
+ void testIsExpected_EmptyPatternMatchesEmptyActualValue_True()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected("", "");
+
+ assertThat(actual)
+ .as("An empty pattern should match an empty actual value.")
+ .isTrue();
+ }
+
+ @Test
+ void testIsExpected_EmptyPatternWithNonEmptyActualValue_False()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected("", "x");
+
+ assertThat(actual)
+ .as("An empty pattern should not match a non-empty actual value.")
+ .isFalse();
+ }
+
+ @Test
+ void testIsExpected_StarQuantifierPatternMatchesEmptyActualValue_True()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected("a*", "");
+
+ assertThat(actual).as(
+ "A pattern that can match zero characters should match an empty actual value.")
+ .isTrue();
+ }
+
+ @Test
+ void testIsExpected_NumericActualValueMatchesDigitPattern_True()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected("\\d+", Integer.valueOf(123));
+
+ assertThat(actual).as(
+ "A non-string actual value should be converted to a string before matching.")
+ .isTrue();
+ }
+
+ @Test
+ void testIsExpected_NumericActualValueDoesNotMatchLetterPattern_False()
+ throws DatabaseUnitException
+ {
+ final boolean actual = isExpected("[A-Za-z]+", Integer.valueOf(123));
+
+ assertThat(actual).as(
+ "A numeric actual value converted to a string should not match a letters-only pattern.")
+ .isFalse();
+ }
+
+ @Test
+ void testIsExpected_ExpectedRegexInvalid_ThrowsDatabaseUnitExceptionWithRowAndColumn()
+ {
+ assertThatExceptionOfType(DatabaseUnitException.class)
+ .as("An invalid regular expression should have thrown DatabaseUnitException.")
+ .isThrownBy(() -> isExpected("[", "abc"))
+ .withMessageContaining(columnName)
+ .withMessageContaining(String.valueOf(rowNum));
+ }
+
+ @Test
+ void testIsExpected_ExpectedRegexInvalid_ExceptionCauseIsPatternSyntaxException()
+ {
+ assertThatExceptionOfType(DatabaseUnitException.class)
+ .as("The thrown exception should wrap the underlying PatternSyntaxException.")
+ .isThrownBy(() -> isExpected("a(", "abc"))
+ .withCauseInstanceOf(PatternSyntaxException.class);
+ }
+
+ @Test
+ void testCompare_ActualValueMatchesPattern_ReturnsNull()
+ throws DatabaseUnitException
+ {
+ final String actual = sut.compare(expectedTable, actualTable, rowNum,
+ columnName, dataType, "\\d+", "42");
+
+ assertThat(actual).as("A matching value should produce no fail message.")
+ .isNull();
+ }
+
+ @Test
+ void testCompare_ActualValueDoesNotMatchPattern_ReturnsFailMessageWithValuesAndPhrase()
+ throws DatabaseUnitException
+ {
+ final String actual = sut.compare(expectedTable, actualTable, rowNum,
+ columnName, dataType, "\\d+", "x");
+
+ assertThat(actual)
+ .as("A non-matching value should produce a fail message naming both values and the fail phrase.")
+ .contains("x").contains("\\d+")
+ .contains("not matching the regular expression");
+ }
+
+ @Test
+ void testCompare_BothNull_ReturnsNull() throws DatabaseUnitException
+ {
+ final String actual = sut.compare(expectedTable, actualTable, rowNum,
+ columnName, dataType, null, null);
+
+ assertThat(actual).as("Both values null should produce no fail message.")
+ .isNull();
+ }
+
+ @Test
+ void testGetFailPhrase_ReturnsNonNullPhrase()
+ {
+ final String actual = sut.getFailPhrase();
+
+ assertThat(actual).as("Should have a fail phrase.").isNotNull();
+ }
+}