Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add withExceptions attribute to @ExpectedToFail #774

Merged
merged 25 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
28a8953
Add `onExceptions` attribute to `@ExpectedToFail`
knutwannheden Oct 7, 2023
17deba7
Address review findings
knutwannheden Oct 19, 2023
4eda00d
Update src/main/java/org/junitpioneer/jupiter/ExpectedToFail.java
knutwannheden Oct 19, 2023
a42532c
Address some more review and add documentation
knutwannheden Oct 19, 2023
7256903
Apply Spotless
knutwannheden Oct 19, 2023
2aa955f
Another round of review feedback
knutwannheden Oct 19, 2023
af5ad33
Make documentation adhere to guidelines
knutwannheden Oct 23, 2023
33a70e8
Better clarify the feature's intent in the documentation
knutwannheden Oct 24, 2023
76467b3
Update `README.md`
knutwannheden Oct 24, 2023
e721325
Update src/main/java/org/junitpioneer/jupiter/ExpectedToFail.java
knutwannheden Nov 7, 2023
7df9a9d
Merge remote-tracking branch 'upstream/main'
knutwannheden Nov 10, 2023
240d2fa
Update `README.adoc`
knutwannheden Nov 10, 2023
ff50d7e
Remove empty `README.md`
knutwannheden Nov 10, 2023
cfa7b06
Correctly order contributions
nipafx Nov 16, 2023
3506e99
Rename `onExceptions` to `withExceptions`
knutwannheden Nov 17, 2023
64c3fb7
Removed repeated warning in documentation
knutwannheden Nov 17, 2023
1f51905
Improved documentation based on review feedback
knutwannheden Nov 17, 2023
7076e18
Update docs/expected-to-fail-tests.adoc
knutwannheden Nov 29, 2023
de8e806
Added more tests and behavior for empty array
knutwannheden Nov 29, 2023
8ec98a1
Throw `ExtensionConfigurationException` on configuration error
knutwannheden Dec 1, 2023
78b9d86
Fix failing test...
knutwannheden Dec 1, 2023
1a8eb5a
Remove star import that got added by IDE
knutwannheden Dec 1, 2023
a43b3ce
Emphasize expected to fail is temporarily
beatngu13 Mar 26, 2024
f5c5e9e
Update assertion
beatngu13 Mar 26, 2024
2edbfa8
Merge remote-tracking branch 'origin/main'
beatngu13 Mar 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ The least we can do is to thank them and list some of their accomplishments here
* https://github.com/eeverman[Eric Everman] added `@RestoreSystemProperties` and `@RestoreEnvironmentVariables` annotations to the https://junit-pioneer.org/docs/system-properties/[System Properties] and https://junit-pioneer.org/docs/environment-variables/[Environment Variables] extensions (#574 / #700)
* https://github.com/meredrica[Florian Westreicher] contributed to the JSON argument source extension (#704 / #724)
* https://github.com/IlyasYOY[Ilya Ilyinykh] found unused demo tests (#791)
* https://github.com/knutwannheden[Knut Wannheden] contributed the `withExceptions` attribute of the https://junit-pioneer.org/docs/expected-to-fail-tests/[`@ExpectedToFail` extension] (#769 / #774)
* https://github.com/petrandreev[Pёtr Andreev] added back support for NULL values to `@CartesianTestExtension` (#764 / #765)

==== 2022
Expand Down
19 changes: 19 additions & 0 deletions docs/expected-to-fail-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,25 @@ A custom message can be provided, explaining why the tested code is not working
include::{demo}[tag=expected_to_fail_message]
----

=== Only Abort on Expected Exceptions
beatngu13 marked this conversation as resolved.
Show resolved Hide resolved

A test that is `@ExpectedToFail` will change its behavior by starting to actually fail, once the code under test behaves correctly.
If the underlying failure changes, though, for example from an assertion error to an exception or from an `UnsupportedOperationException` of a formerly missing implementation to a runtime exception of buggy implementation, the `@ExpectedToFail`-test will keep passing.

To better react to such changes, `@ExpectedToFail` has an attribute `withExceptions` that can be used to enumerate the exceptions which when thrown will result in an aborted (and thus passing) test.
Any other exception thrown by the code under test will result in a failing test.

In the following example a test case for `productionFeature()` has been implemented.
While the test is fully implemented, the production code has been stubbed and throws an `UnsupportedOperationException` to indicate this.

[source,java,indent=0]
----
include::{demo}[tag=expected_to_fail_withexception]
----

Once `productionFeature()` is implemented, `@ExpectedToFail` will fail the test, as no `UnsupportedOperationException` is thrown anymore.
By using `withExceptions` you can thus prevent "masking" a faulty implementation (e.g. when a value other rather than `10` is returned) with an aborted test (which would be the result when no `withExceptions` is set).

== Thread-Safety

This extension is safe to use during https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution[parallel test execution].
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,17 @@ private int brokenMethod() {
return 0;
}

// tag::expected_to_fail_withexception[]
@Test
@ExpectedToFail(withExceptions = UnsupportedOperationException.class)
void testProductionFeature() {
int actual = productionFeature();
assertEquals(10, actual);
}

private int productionFeature() {
throw new UnsupportedOperationException("productionFeature() is not yet implemented");
}
// end::expected_to_fail_withexception[]

}
14 changes: 14 additions & 0 deletions src/main/java/org/junitpioneer/jupiter/ExpectedToFail.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@
* This helps to avoid creating duplicate tests by accident and counteracts the accumulation
* of disabled tests over time.</p>
*
* <p>Further, the {@link #withExceptions()} attribute can be used to restrict the extension's behavior
* to specific exceptions. That is, only if the test method ends up throwing one of the specified exceptions
* will the test be aborted. This can, for example, be used when the production code temporarily throws
* an {@link UnsupportedOperationException} because some feature has not been implemented yet, but the
* test method is already implemented and should not fail on a failing assertion.
* </p>
*
* <p>The annotation can only be used on methods and as meta-annotation on other annotation types.
* Similar to {@code @Disabled}, it has to be used in addition to a "testable" annotation, such
* as {@link org.junit.jupiter.api.Test @Test}. Otherwise the annotation has no effect.</p>
Expand Down Expand Up @@ -68,4 +75,11 @@
*/
String value() default "";

/**
* Specifies which exceptions are expected to be thrown and will cause the test to be aborted rather than fail.
* An empty array is considered a configuration error and will cause the test to fail. Instead, consider leaving
* the attribute set to the default value when any exception should cause the test to be aborted.
*/
Class<? extends Throwable>[] withExceptions() default { Throwable.class };

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
import static org.junit.jupiter.api.Assertions.fail;

import java.lang.reflect.Method;
import java.util.stream.Stream;

import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.ExtensionConfigurationException;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.InvocationInterceptor;
import org.junit.jupiter.api.extension.ReflectiveInvocationContext;
Expand All @@ -31,6 +33,11 @@ public void interceptTestMethod(Invocation<Void> invocation, ReflectiveInvocatio

private static void invokeAndInvertResult(Invocation<Void> invocation, ExtensionContext extensionContext)
throws Throwable {
ExpectedToFail expectedToFail = getExpectedToFailAnnotation(extensionContext);
if (expectedToFail.withExceptions().length == 0) {
throw new ExtensionConfigurationException("@ExpectedToFail withExceptions must not be empty");
}

try {
invocation.proceed();
// at this point, the invocation succeeded, so we'd want to call `fail(...)`,
Expand All @@ -42,7 +49,11 @@ private static void invokeAndInvertResult(Invocation<Void> invocation, Extension
throw t;
}

String message = getExpectedToFailAnnotation(extensionContext).value();
if (Stream.of(expectedToFail.withExceptions()).noneMatch(clazz -> clazz.isInstance(t))) {
fail("Test marked as temporarily 'expected to fail' failed with an unexpected exception", t);
}

String message = expectedToFail.value();
if (message.isEmpty()) {
message = "Test marked as temporarily 'expected to fail' failed as expected";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtensionConfigurationException;
import org.junitpioneer.testkit.ExecutionResults;
import org.junitpioneer.testkit.PioneerTestKit;
import org.opentest4j.AssertionFailedError;
Expand Down Expand Up @@ -101,6 +102,52 @@ void failsOnWorkingTest() {
.hasMessage("Test marked as 'expected to fail' succeeded; remove @ExpectedToFail from it");
}

@Test
void doesNotAbortOnTestThrowingExpectedException() {
ExecutionResults results = PioneerTestKit
.executeTestMethod(ExpectedToFailTestCases.class, "withExceptionsExpected");
assertThat(results)
.hasSingleStartedTest()
.whichAborted()
.withExceptionInstanceOf(TestAbortedException.class)
.hasMessage("Test marked as temporarily 'expected to fail' failed as expected")
.hasCauseInstanceOf(UnsupportedOperationException.class);
}

@Test
void failsOnTestThrowingUnexpectedException() {
ExecutionResults results = PioneerTestKit
.executeTestMethod(ExpectedToFailTestCases.class, "withExceptionsUnexpected");
assertThat(results)
.hasSingleStartedTest()
.whichFailed()
.withExceptionInstanceOf(AssertionFailedError.class)
.hasMessage("Test marked as temporarily 'expected to fail' failed with an unexpected exception")
.hasCauseInstanceOf(AssertionFailedError.class);
}

@Test
void failsOnWorkingTestWithExpectedException() {
ExecutionResults results = PioneerTestKit
.executeTestMethod(ExpectedToFailTestCases.class, "withExceptionsWorking");
assertThat(results)
.hasSingleStartedTest()
.whichFailed()
.withExceptionInstanceOf(AssertionFailedError.class)
.hasMessage("Test marked as 'expected to fail' succeeded; remove @ExpectedToFail from it");
}

@Test
void failsOnWorkingTestWithEmptyExpectedExceptions() {
ExecutionResults results = PioneerTestKit
.executeTestMethod(ExpectedToFailTestCases.class, "withExceptionsEmpty");
assertThat(results)
.hasSingleStartedTest()
.whichFailed()
.withExceptionInstanceOf(ExtensionConfigurationException.class)
.hasMessage("@ExpectedToFail withExceptions must not be empty");
}

@Test
void doesNotAbortOnBeforeEachTestFailure() {
ExecutionResults results = PioneerTestKit
Expand Down Expand Up @@ -219,6 +266,28 @@ void working() {
// Does not cause failure or error
}

@Test
@ExpectedToFail(withExceptions = { IllegalStateException.class, UnsupportedOperationException.class })
void withExceptionsExpected() {
throw new UnsupportedOperationException();
}

@Test
@ExpectedToFail(withExceptions = UnsupportedOperationException.class)
void withExceptionsUnexpected() {
fail();
}

@Test
@ExpectedToFail(withExceptions = UnsupportedOperationException.class)
void withExceptionsWorking() {
}

@Test
@ExpectedToFail(withExceptions = {})
void withExceptionsEmpty() {
}

}

/**
Expand Down