Skip to content

Commit d22a3c3

Browse files
matchers: throw exception matcher more details on failure (#1591)
1 parent 7347d13 commit d22a3c3

6 files changed

Lines changed: 122 additions & 19 deletions

File tree

webtau-core-groovy/src/test/groovy/org/testingisdocumenting/webtau/expectation/code/ValueChangeCodeMatcherGroovyTest.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import org.testingisdocumenting.webtau.data.DbEntity
2121

2222
import static org.testingisdocumenting.webtau.Matchers.*
2323

24-
class ValueValueChangeCodeMatcherGroovyTest {
24+
class ValueChangeCodeMatcherGroovyTest {
2525
@Test
2626
void "change java bean single property"() {
2727
def dbEntity = new DbEntity()

webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/ActualCode.java

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,14 @@
1717

1818
package org.testingisdocumenting.webtau.expectation;
1919

20-
import org.testingisdocumenting.webtau.reporter.StepReportOptions;
21-
import org.testingisdocumenting.webtau.reporter.TokenizedMessage;
22-
import org.testingisdocumenting.webtau.reporter.WebTauStep;
23-
import org.testingisdocumenting.webtau.reporter.WebTauStepClassifiers;
20+
import org.testingisdocumenting.webtau.data.ValuePath;
21+
import org.testingisdocumenting.webtau.data.converters.ValueConverter;
22+
import org.testingisdocumenting.webtau.data.render.PrettyPrinter;
23+
import org.testingisdocumenting.webtau.expectation.stepoutput.ValueMatcherStepOutput;
24+
import org.testingisdocumenting.webtau.reporter.*;
25+
26+
import java.util.Set;
27+
import java.util.function.Supplier;
2428

2529
import static org.testingisdocumenting.webtau.WebTauCore.*;
2630
import static org.testingisdocumenting.webtau.reporter.WebTauStep.*;
@@ -48,24 +52,28 @@ public void shouldNot(CodeMatcher codeMatcher) {
4852
() -> shouldNotStep(codeMatcher), StepReportOptions.REPORT_ALL);
4953
}
5054

51-
private void shouldStep(CodeMatcher codeMatcher) {
55+
private boolean shouldStep(CodeMatcher codeMatcher) {
5256
boolean matches = codeMatcher.matches(actual);
5357

5458
if (matches) {
5559
handleMatch(codeMatcher);
5660
} else {
5761
handleMismatch(codeMatcher, codeMatcher.mismatchedTokenizedMessage(actual));
5862
}
63+
64+
return matches;
5965
}
6066

61-
private void shouldNotStep(CodeMatcher codeMatcher) {
67+
private boolean shouldNotStep(CodeMatcher codeMatcher) {
6268
boolean matches = codeMatcher.negativeMatches(actual);
6369

6470
if (matches) {
6571
handleMatch(codeMatcher);
6672
} else {
6773
handleMismatch(codeMatcher, codeMatcher.negativeMismatchedTokenizedMessage(actual));
6874
}
75+
76+
return matches;
6977
}
7078

7179
private void handleMatch(CodeMatcher codeMatcher) {
@@ -83,7 +91,7 @@ private void handleMismatch(CodeMatcher codeMatcher, TokenizedMessage message) {
8391
private void executeStep(CodeMatcher codeMatcher,
8492
boolean isNegative,
8593
TokenizedMessage messageStart,
86-
Runnable expectationValidation,
94+
Supplier<Object> expectationValidation,
8795
StepReportOptions stepReportOptions) {
8896
TokenizedMessage codeDescription = tokenizedMessage().id("code");
8997
WebTauStep step = createStep(
@@ -93,7 +101,28 @@ private void executeStep(CodeMatcher codeMatcher,
93101
.add(isNegative ? codeMatcher.negativeMatchedTokenizedMessage(actual) : codeMatcher.matchedTokenizedMessage(actual)),
94102
expectationValidation);
95103
step.setClassifier(WebTauStepClassifiers.MATCHER);
104+
step.setStepOutputFunc((matched) -> {
105+
Set<ValuePath> pathsToDecorate = isNegative ? codeMatcher.matchedPaths() : codeMatcher.mismatchedPaths();
106+
return createStepOutput(codeMatcher.prettyPrintValueRootPath(), codeMatcher.stepOutputValueToPrettyPrint(),
107+
matched,
108+
pathsToDecorate);
109+
});
96110

97111
step.execute(stepReportOptions);
98112
}
113+
114+
private WebTauStepOutput createStepOutput(ValuePath valueRootPath,
115+
Object valueToPrettyPrint,
116+
Object isMatched,
117+
Set<ValuePath> pathsToDecorate) {
118+
119+
if (Boolean.TRUE.equals(isMatched) || !PrettyPrinter.isPrettyPrintable(valueToPrettyPrint) || valueToPrettyPrint == null) {
120+
return WebTauStepOutput.EMPTY;
121+
}
122+
123+
return new ValueMatcherStepOutput(valueRootPath,
124+
valueToPrettyPrint,
125+
ValueConverter.EMPTY,
126+
pathsToDecorate);
127+
}
99128
}

webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/CodeMatcher.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,22 @@
1919
import org.testingisdocumenting.webtau.data.ValuePath;
2020
import org.testingisdocumenting.webtau.reporter.TokenizedMessage;
2121

22+
import java.util.Collections;
23+
import java.util.Set;
24+
2225
public interface CodeMatcher {
26+
default Object stepOutputValueToPrettyPrint() { return null; }
27+
28+
default ValuePath prettyPrintValueRootPath() { return ValuePath.UNDEFINED; }
29+
30+
/**
31+
* match paths for optional matcher step output
32+
* @return paths that matched
33+
*/
34+
default Set<ValuePath> matchedPaths() {
35+
return Collections.emptySet();
36+
}
37+
2338
/**
2439
* @return about to start matching message
2540
*/
@@ -31,6 +46,14 @@ public interface CodeMatcher {
3146
*/
3247
TokenizedMessage matchedTokenizedMessage(CodeBlock codeBlock);
3348

49+
/**
50+
* mismatch paths for optional matcher step output
51+
* @return paths that matched
52+
*/
53+
default Set<ValuePath> mismatchedPaths() {
54+
return Collections.emptySet();
55+
}
56+
3457
/**
3558
* @param codeBlock matching code block
3659
* @return mismatch message

webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/code/ThrowExceptionMatcher.java

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,28 @@
1717

1818
package org.testingisdocumenting.webtau.expectation.code;
1919

20+
import org.testingisdocumenting.webtau.data.ValuePath;
21+
import org.testingisdocumenting.webtau.data.converters.ValueConverter;
2022
import org.testingisdocumenting.webtau.expectation.*;
2123
import org.testingisdocumenting.webtau.expectation.equality.CompareToComparator;
2224
import org.testingisdocumenting.webtau.reporter.TokenizedMessage;
2325
import org.testingisdocumenting.webtau.reporter.stacktrace.StackTraceUtils;
2426

2527
import java.lang.reflect.UndeclaredThrowableException;
2628
import java.util.HashMap;
29+
import java.util.LinkedHashMap;
2730
import java.util.Map;
31+
import java.util.Set;
2832
import java.util.regex.Pattern;
2933
import java.util.stream.Stream;
3034

3135
import static org.testingisdocumenting.webtau.WebTauCore.*;
3236

3337
public class ThrowExceptionMatcher implements CodeMatcher, ExpectedValuesAware, ActualValueAware {
38+
private static final ValuePath comparisonActualPath = createActualPath("exception");
39+
private static final String MESSAGE_KEY = "message";
40+
private static final String CLASS_KEY = "class";
41+
3442
private Object expectedMessageMatcherOrValue;
3543
private Class<?> expectedClass;
3644
private String thrownMessage;
@@ -69,6 +77,32 @@ public ThrowExceptionMatcher(Class<?> expectedClass, ValueMatcher expectedMessag
6977
this.expectedMessageMatcherOrValue = expectedMessageMatcher;
7078
}
7179

80+
@Override
81+
public Object stepOutputValueToPrettyPrint() {
82+
ValueConverter valueConverter = comparator.createValueConverter();
83+
Map<String, Object> result = new LinkedHashMap<>();
84+
if (thrownMessage != null) {
85+
result.put(MESSAGE_KEY, valueConverter.convertValue(
86+
prettyPrintValueRootPath().property(MESSAGE_KEY), thrownMessage));
87+
}
88+
89+
if (thrownClass != null) {
90+
result.put(CLASS_KEY, valueConverter.convertValue(
91+
prettyPrintValueRootPath().property(CLASS_KEY), thrownClass));
92+
}
93+
94+
if (thrownExceptionStackTrace != null) {
95+
result.put("stack trace", thrownExceptionStackTrace);
96+
}
97+
98+
return result.isEmpty() ? null : result;
99+
}
100+
101+
@Override
102+
public ValuePath prettyPrintValueRootPath() {
103+
return comparisonActualPath;
104+
}
105+
72106
@Override
73107
public TokenizedMessage matchingTokenizedMessage() {
74108
return tokenizedMessage().matcher("to throw exception").value(buildExpectedValueForMessage());
@@ -85,12 +119,7 @@ public TokenizedMessage mismatchedTokenizedMessage(CodeBlock codeBlock) {
85119
return tokenizedMessage().error("no exception was thrown");
86120
}
87121

88-
TokenizedMessage message = comparator.generateEqualMismatchReport();
89-
if (thrownExceptionStackTrace != null) {
90-
message.newLine().none("stack trace").colon().newLine().error(thrownExceptionStackTrace);
91-
}
92-
93-
return message;
122+
return comparator.generateEqualMismatchReport();
94123
}
95124

96125
@Override
@@ -124,7 +153,17 @@ public boolean matches(CodeBlock codeBlock) {
124153
Map<String, Object> actualThrownAsMap = buildThrownToUseForCompare();
125154
Map<String, Object> expectedAsMap = buildExpectedMapToUseForCompare();
126155

127-
return comparator.compareIsEqual(createActualPath("exception"), actualThrownAsMap, expectedAsMap);
156+
return comparator.compareIsEqual(comparisonActualPath, actualThrownAsMap, expectedAsMap);
157+
}
158+
159+
@Override
160+
public Set<ValuePath> matchedPaths() {
161+
return comparator.generateEqualMatchPaths();
162+
}
163+
164+
@Override
165+
public Set<ValuePath> mismatchedPaths() {
166+
return comparator.generateEqualMismatchPaths();
128167
}
129168

130169
@Override
@@ -158,11 +197,11 @@ private Map<String, Object> buildExpectedMapToUseForCompare() {
158197
private Map<String, Object> createMap(Object message, Class<?> aClass) {
159198
Map<String, Object> result = new HashMap<>();
160199
if (expectedMessageMatcherOrValue != null) {
161-
result.put("message", message);
200+
result.put(MESSAGE_KEY, message);
162201
}
163202

164203
if (expectedClass != null) {
165-
result.put("class", aClass);
204+
result.put(CLASS_KEY, aClass);
166205
}
167206

168207
return result;

webtau-core/src/test/groovy/org/testingisdocumenting/webtau/expectation/code/ThrowExceptionMatcherGroovyTest.groovy

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ class ThrowExceptionMatcherGroovyTest {
3838
}
3939
}
4040

41+
@Test
42+
void "multiline exception mismatch should contain additional details about specific line"() {
43+
runExpectExceptionAndValidateOutput(AssertionError, contain("**line two**")) {
44+
code {
45+
throw new RuntimeException('line one\nline two\n')
46+
} should throwException('line one\nline Two')
47+
}
48+
}
49+
4150
@Test
4251
void "should validate exception message using regexp"() {
4352
runExpectExceptionAndValidateOutput(AssertionError, contain('exception.message: actual string: error message\n' +
@@ -119,8 +128,10 @@ class ThrowExceptionMatcherGroovyTest {
119128

120129
@Test
121130
void "should add exception stack trace when mismatched"() {
122-
runExpectExceptionAndValidateOutput(AssertionError.class, contain("stack trace:\n" +
123-
" java.lang.RuntimeException: java.lang.IllegalArgumentException: negative not allowed")) {
131+
runExpectExceptionAndValidateOutput(AssertionError.class,
132+
containAll(
133+
"stack trace",
134+
"java.lang.RuntimeException: java.lang.IllegalArgumentException: negative not allowed")) {
124135
code {
125136
businessLogicStart()
126137
} should throwException(NullPointerException, 'error message1')
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Add: additional details on failed messages for `throwException` matcher

0 commit comments

Comments
 (0)