From 0fcc50ff66ba7e27868d6c8cd277bf5c4a29ecb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20Br=C3=BCnings?= Date: Sat, 28 Dec 2024 15:01:06 +0100 Subject: [PATCH] Allow `@FailsWith` to assert error message --- docs/release_notes.adoc | 1 + .../builtin/FailsWithInterceptor.java | 19 +++++++++- .../src/main/java/spock/lang/FailsWith.java | 9 +++++ .../smoke/extension/FailsWithExtension.groovy | 37 +++++++++++++++++-- 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/docs/release_notes.adoc b/docs/release_notes.adoc index b489c3b179..7273f0e70c 100644 --- a/docs/release_notes.adoc +++ b/docs/release_notes.adoc @@ -19,6 +19,7 @@ include::include.adoc[] * Add `IStatelessAnnotationDrivenExtension` to allow a single extension instance to be reused across all specifications spockPull:2055[] * Add new well-known versions to `Jvm` helper to support versions up to 29 spockPull:2057[] ** Built-in extensions have been updated to use this new interface where applicable. +* Add support for `@FailsWith` to assert an exception message spockIssue:2063[] * Improve `@Timeout` extension will now use virtual threads if available spockPull:1986[] * Improve mock argument matching, types constraints or arguments in interactions can now handle primitive types like `_ as int` spockIssue:1974[] * Improve `verifyEach` to accept an optional second index parameter for the assertion block closure spockPull:2043[] diff --git a/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/FailsWithInterceptor.java b/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/FailsWithInterceptor.java index 0a309e37c5..78931bb018 100644 --- a/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/FailsWithInterceptor.java +++ b/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/FailsWithInterceptor.java @@ -18,8 +18,12 @@ import org.spockframework.runtime.*; import org.spockframework.runtime.extension.*; +import org.spockframework.runtime.model.TextPosition; + import spock.lang.FailsWith; +import java.util.Arrays; + /** * * @author Peter Niederwieser @@ -44,7 +48,20 @@ public void intercept(IMethodInvocation invocation) throws Throwable { } private boolean checkException(Throwable t) { + if(matchesException(t)) { + if (!failsWith.expectedMessage().isEmpty() && !failsWith.expectedMessage().equals(t.getMessage())) { + throw new ConditionNotSatisfiedError(new Condition( + Arrays.asList(t, t.getMessage(), failsWith.expectedMessage()), + "e.value == expectedMessage", + TextPosition.create(-1, -1), null, null, null)); + } + return true; + } + return false; + } + + private boolean matchesException(Throwable t) { return failsWith.value().isInstance(t) - || (t instanceof ConditionFailedWithExceptionError && failsWith.value().isInstance(t.getCause())); + || (t instanceof ConditionFailedWithExceptionError && failsWith.value().isInstance(t.getCause())); } } diff --git a/spock-core/src/main/java/spock/lang/FailsWith.java b/spock-core/src/main/java/spock/lang/FailsWith.java index fac3e08e15..2e57a53ce3 100644 --- a/spock-core/src/main/java/spock/lang/FailsWith.java +++ b/spock-core/src/main/java/spock/lang/FailsWith.java @@ -50,4 +50,13 @@ * @return the reason for the failure */ String reason() default "unknown"; + + /** + * The expected message of the exception. + *

+ * If the value is empty, the message is not checked. + * + * @since 2.4 + */ + String expectedMessage() default ""; } diff --git a/spock-specs/src/test/groovy/org/spockframework/smoke/extension/FailsWithExtension.groovy b/spock-specs/src/test/groovy/org/spockframework/smoke/extension/FailsWithExtension.groovy index 21c35459e5..47390e7caa 100644 --- a/spock-specs/src/test/groovy/org/spockframework/smoke/extension/FailsWithExtension.groovy +++ b/spock-specs/src/test/groovy/org/spockframework/smoke/extension/FailsWithExtension.groovy @@ -22,11 +22,13 @@ import org.spockframework.runtime.InvalidSpecException import spock.lang.FailsWith import spock.lang.Specification +import org.spockframework.runtime.SpockComparisonFailure + /** * * @author Peter Niederwieser */ -class FailsWithOnMethod extends Specification { +class FailsWithOnMethod extends EmbeddedSpecification { @FailsWith(IndexOutOfBoundsException) def ex1() { given: @@ -34,7 +36,10 @@ class FailsWithOnMethod extends Specification { foo.get(0) } - @FailsWith(Exception) + @FailsWith( + value = Exception, + expectedMessage = "Index 0 out of bounds for length 0" + ) def ex2() { given: def foo = [] @@ -45,6 +50,33 @@ class FailsWithOnMethod extends Specification { expect: true } + def "@FailsWith can assert exception message"() { + when: + runner.runSpecBody """ + @FailsWith( + value = Exception, + expectedMessage = "Index 1 out of bounds for length 1" + ) + def foo() { + given: + def foo = [] + foo.get(0) + } + """ + + then: + SpockComparisonFailure e = thrown() + def expected = """Condition not satisfied: + +e.value == expectedMessage +| | | +| | Index 1 out of bounds for length 1 +| Index 0 out of bounds for length 0 +java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0""" + + e.message.startsWith(expected) + + } @FailsWith(ConditionFailedWithExceptionError) def "can handle ConditionFailedWithExceptionError"() { @@ -117,4 +149,3 @@ class MySpec extends Specification { e.message == "@FailsWith needs to refer to an exception type, but does refer to 'java.util.List'" } } -