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 ExpectedToFail test extension (#551 / #676) #676
Merged
nipafx
merged 11 commits into
junit-pioneer:main
from
Marcono1234:feature/expected-to-fail-extension
Nov 11, 2022
Merged
Changes from 7 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
a132c5f
Add NotWorking test extension
Marcono1234 8ef076d
Address review feedback
Marcono1234 3c6aa7c
Merge remote-tracking branch 'remotes/origin/main' into feature/not-w…
Marcono1234 9b49f78
Address review feedback
Marcono1234 49a4d93
Merge branch 'main' into feature/not-working-extension
Marcono1234 717f39b
Implement extension with JUnit's InvocationInterceptor
Marcono1234 5ecce32
Rename extension to ExpectedToFail
Marcono1234 543da45
Clarify filtering comments in PioneerAssert
Marcono1234 9731701
Further polish documentation
11e03c2
Reorder code a bit and remove a constructor
8683900
Update docs/expected-to-fail-tests.adoc
nipafx File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
:page-title: Expected to Fail Tests | ||
:page-description: Extends JUnit Jupiter with `@ExpectedToFail`, which marks a test method as temporarily 'expected to fail' | ||
:xp-demo-dir: ../src/demo/java | ||
:demo: {xp-demo-dir}/org/junitpioneer/jupiter/ExpectedToFailExtensionDemo.java | ||
|
||
Often tests fail due to a bug in the tested application or in used dependencies. | ||
Traditionally such a test method would be annotated with JUnit's `@Disabled`. | ||
However, this has the following disadvantages when the bug causing the test failure is fixed: | ||
|
||
* the developer might not notice the existing test method and create a new one | ||
* the existing test method might not be noticed and remains disabled for a long time after the bug has been fixed, adding no value for the project | ||
|
||
`@ExpectedToFail` tries to solve these issues. | ||
Unlike `@Disabled` it still executes the annotated test method but aborts the test if a test failure or error occurs. | ||
However, if the test is executed successfully the `@ExpectedToFail` annotation will cause a test failure because the test _is working_. | ||
This lets the developer know that they have fixed the bug (possibly by accident) and that they can now remove the `@ExpectedToFail` annotation from the test method. | ||
|
||
The annotation can only be used on methods and as meta-annotation on other annotation types. | ||
Similar to `@Disabled`, it has to be used in addition to a "testable" annotation, such as `@Test`. | ||
Otherwise the annotation has no effect. | ||
|
||
IMPORTANT: This annotation is *not* intended as a way to mark test methods which intentionally cause exceptions. | ||
Such test methods should use https://junit.org/junit5/docs/current/api/org.junit.jupiter.api/org/junit/jupiter/api/Assertions.html#assertThrows(java.lang.Class,org.junit.jupiter.api.function.Executable)[JUnit's `assertThrows`] or similar means to explicitly test for a specific exception class being thrown by a specific action. | ||
|
||
== Basic Use | ||
|
||
The test is aborted because the tested method `brokenMethod()` returns an incorrect result. | ||
|
||
[source,java,indent=0] | ||
---- | ||
include::{demo}[tag=expected_to_fail] | ||
---- | ||
|
||
A custom message can be provided, explaining why the tested code is not working as intended at the moment. | ||
|
||
[source,java,indent=0] | ||
---- | ||
include::{demo}[tag=expected_to_fail_message] | ||
---- | ||
|
||
== Thread-Safety | ||
|
||
This extension is thread-safe. |
41 changes: 41 additions & 0 deletions
41
src/demo/java/org/junitpioneer/jupiter/ExpectedToFailExtensionDemo.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
/* | ||
* Copyright 2016-2022 the original author or authors. | ||
* | ||
* All rights reserved. This program and the accompanying materials are | ||
* made available under the terms of the Eclipse Public License v2.0 which | ||
* accompanies this distribution and is available at | ||
* | ||
* http://www.eclipse.org/legal/epl-v20.html | ||
*/ | ||
|
||
package org.junitpioneer.jupiter; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
|
||
import org.junit.jupiter.api.Test; | ||
|
||
public class ExpectedToFailExtensionDemo { | ||
|
||
// tag::expected_to_fail[] | ||
@ExpectedToFail | ||
@Test | ||
void test() { | ||
int actual = brokenMethod(); | ||
assertEquals(10, actual); | ||
} | ||
// end::expected_to_fail[] | ||
|
||
// tag::expected_to_fail_message[] | ||
@ExpectedToFail("Implementation bug in brokenMethod()") | ||
@Test | ||
void doSomething() { | ||
int actual = brokenMethod(); | ||
assertEquals(10, actual); | ||
} | ||
// end::expected_to_fail_message[] | ||
|
||
private int brokenMethod() { | ||
return 0; | ||
} | ||
|
||
} |
74 changes: 74 additions & 0 deletions
74
src/main/java/org/junitpioneer/jupiter/ExpectedToFail.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
/* | ||
* Copyright 2016-2022 the original author or authors. | ||
* | ||
* All rights reserved. This program and the accompanying materials are | ||
* made available under the terms of the Eclipse Public License v2.0 which | ||
* accompanies this distribution and is available at | ||
* | ||
* http://www.eclipse.org/legal/epl-v20.html | ||
*/ | ||
|
||
package org.junitpioneer.jupiter; | ||
|
||
import static java.lang.annotation.ElementType.ANNOTATION_TYPE; | ||
import static java.lang.annotation.ElementType.METHOD; | ||
import static java.lang.annotation.RetentionPolicy.RUNTIME; | ||
|
||
import java.lang.annotation.Documented; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.Target; | ||
|
||
import org.junit.jupiter.api.extension.ExtendWith; | ||
|
||
/** | ||
* {@code @ExpectedToFail} is a JUnit Jupiter extension to mark test methods as temporarily | ||
* 'expected to fail'. | ||
* Such test methods will still be executed but when they result in a test failure or error | ||
* the test will be aborted. | ||
* However, if the test method unexpectedly executes successfully, it is marked as failure | ||
* to let the developer know that the test is now successful and that the {@code @ExpectedToFail} | ||
* annotation can be removed. | ||
* | ||
* <p>The big difference compared to JUnit's {@link org.junit.jupiter.api.Disabled @Disabled} | ||
* annotation is that the developer is informed as soon as a test is successful again. | ||
* This helps avoiding writing duplicate tests by accident and counteracts the accumulation | ||
* of disabled tests over time. | ||
* | ||
* <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><b>Important:</b> This annotation is <b>not</b> intended as a way to mark test methods | ||
* which intentionally cause exceptions. | ||
* Such test methods should use {@link org.junit.jupiter.api.Assertions#assertThrows(Class, org.junit.jupiter.api.function.Executable) assertThrows} | ||
* or similar means to explicitly test for a specific exception class being thrown by a | ||
* specific action. | ||
* | ||
* <p>For more details and examples, see | ||
* <a href="https://junit-pioneer.org/docs/expected-to-fail-tests/" target="_top">the documentation on <code>@ExpectedToFail</code></a>. | ||
* </p> | ||
* | ||
* @see org.junit.jupiter.api.Disabled | ||
*/ | ||
/* | ||
* Implementation note: | ||
* Only supports METHOD and ANNOTATION_TYPE as targets but not test classes because there | ||
* it is not clear what the 'correct' behavior would be when only a few test methods | ||
* execute successfully. Would the developer then have to remove the @ExpectedToFail annotation | ||
* from the test class and annotate methods individually? | ||
*/ | ||
@Documented | ||
@Retention(RUNTIME) | ||
@Target({ METHOD, ANNOTATION_TYPE }) | ||
@ExtendWith(ExpectedToFailExtension.class) | ||
public @interface ExpectedToFail { | ||
|
||
/** | ||
* Defines the message to show when a test is aborted because it is failing. | ||
* This can be used for example to briefly explain why the tested code is not working | ||
* as intended at the moment. | ||
* An empty string (the default) causes a generic default message to be used. | ||
*/ | ||
String value() default ""; | ||
|
||
} |
82 changes: 82 additions & 0 deletions
82
src/main/java/org/junitpioneer/jupiter/ExpectedToFailExtension.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
/* | ||
* Copyright 2016-2022 the original author or authors. | ||
* | ||
* All rights reserved. This program and the accompanying materials are | ||
* made available under the terms of the Eclipse Public License v2.0 which | ||
* accompanies this distribution and is available at | ||
* | ||
* http://www.eclipse.org/legal/epl-v20.html | ||
*/ | ||
|
||
package org.junitpioneer.jupiter; | ||
|
||
import static org.junit.jupiter.api.Assertions.fail; | ||
|
||
import java.lang.reflect.Method; | ||
|
||
import org.junit.jupiter.api.extension.Extension; | ||
import org.junit.jupiter.api.extension.ExtensionContext; | ||
import org.junit.jupiter.api.extension.InvocationInterceptor; | ||
import org.junit.jupiter.api.extension.ReflectiveInvocationContext; | ||
import org.junit.platform.commons.support.AnnotationSupport; | ||
import org.opentest4j.TestAbortedException; | ||
|
||
class ExpectedToFailExtension implements Extension, InvocationInterceptor { | ||
|
||
/** | ||
* No-arg constructor for JUnit to be able to create an instance. | ||
*/ | ||
public ExpectedToFailExtension() { | ||
} | ||
|
||
private static ExpectedToFail getExpectedToFailAnnotation(ExtensionContext context) { | ||
return AnnotationSupport | ||
.findAnnotation(context.getRequiredTestMethod(), ExpectedToFail.class) | ||
.orElseThrow(() -> new IllegalStateException("@ExpectedToFail is missing.")); | ||
|
||
} | ||
|
||
/** | ||
* Returns whether the exception should be preserved and reported as is instead | ||
* of considering it an 'expected to fail' exception. | ||
* | ||
* <p>This method is used for exceptions which abort test execution and should | ||
* have higher precedence than aborted exceptions thrown by this extension. | ||
*/ | ||
private static boolean shouldPreserveException(Throwable t) { | ||
// Note: Ideally would use the same logic JUnit uses to determine if exception is aborting | ||
// execution, see its class OpenTest4JAndJUnit4AwareThrowableCollector | ||
return TestAbortedException.class.isInstance(t); | ||
} | ||
|
||
private static <T> T invokeAndInvertResult(Invocation<T> invocation, ExtensionContext extensionContext) | ||
throws Throwable { | ||
try { | ||
invocation.proceed(); | ||
// if no exception was thrown fall through and call fail(...) eventually | ||
} | ||
catch (Throwable t) { | ||
if (shouldPreserveException(t)) { | ||
throw t; | ||
} | ||
|
||
ExpectedToFail annotation = getExpectedToFailAnnotation(extensionContext); | ||
|
||
String message = annotation.value(); | ||
if (message.isEmpty()) { | ||
message = "Test marked as temporarily 'expected to fail' failed as expected"; | ||
} | ||
|
||
throw new TestAbortedException(message, t); | ||
} | ||
|
||
return fail("Test marked as 'expected to fail' succeeded; remove @ExpectedToFail from it"); | ||
} | ||
|
||
@Override | ||
public void interceptTestMethod(Invocation<Void> invocation, ReflectiveInvocationContext<Method> invocationContext, | ||
ExtensionContext extensionContext) throws Throwable { | ||
invokeAndInvertResult(invocation, extensionContext); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have implemented this as separate method in case more
InvocationInterceptor
methods will be overridden in the future.