Skip to content

Commit 304b325

Browse files
authored
Add FailAt Extension (#549 / #814)
The FailAt Extension provides to fail annotated test methods or classes when a given date is reached. Closes #549 PR: #814
1 parent 8798dbe commit 304b325

8 files changed

Lines changed: 376 additions & 1 deletion

File tree

docs/disabled-until.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ include::{demo}[tag=disable_until_simple]
3232
The `date` parameter must be a string in the date format specified by https://en.m.wikipedia.org/wiki/ISO_8601[ISO 8601], e.g. "1985-10-26".
3333
Invalid or unparsable date strings lead to an `ExtensionConfigurationException`.
3434

35-
The `@DisabledUntil annotation may optionally be declared with a reason to document why the annotated test class or test method is disabled:
35+
The `@DisabledUntil` annotation may optionally be declared with a reason to document why the annotated test class or test method is disabled:
3636

3737
[source,java,indent=0]
3838
----

docs/docs-nav.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
url: /docs/disable-parameterized-tests/
2424
- title: "Expected-to-Fail Tests"
2525
url: /docs/expected-to-fail-tests/
26+
- title: "Fail Test at a Date"
27+
url: /docs/fail-at/
2628
- title: "Injecting Resources"
2729
url: /docs/resources/
2830
- title: "Injecting Temporary Directories"

docs/fail-at.adoc

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
:page-title: Fail test after certain date
2+
:page-description: The JUnit 5 (Jupiter) extension `@FailAt` fails a test after a certain date
3+
:xp-demo-dir: ../src/demo/java
4+
:demo: {xp-demo-dir}/org/junitpioneer/jupiter/FailAtExtensionDemo.java
5+
6+
It's sometimes useful to fail a test after a certain date.
7+
One can imagine many reasons for doing so, maybe a remote dependency of the test is not licenced anymore.
8+
9+
The `@FailAt` annotation is perfectly suited for such cases.
10+
The test will fail when the given date is reached.
11+
12+
[WARNING]
13+
====
14+
This annotation allows the user to move an https://junit.org/junit5/docs/current/user-guide/#writing-tests-assumptions[assumption] out of one or multiple test method's code into the annotation.
15+
But this comes at a cost:
16+
Applying `@FailAt` can make the test suite non-reproducible.
17+
If a passing test is run again after the specified date, that build would fail.
18+
A report entry is issued for every test that does not fail until a certain date.
19+
====
20+
21+
== Usage
22+
23+
To mark a test to fail at a given date, add the `@FailAt` annotation like so:
24+
25+
[source,java,indent=0]
26+
----
27+
include::{demo}[tag=fail_at_simple]
28+
----
29+
30+
The `date` parameter must be a string in the date format specified by https://en.m.wikipedia.org/wiki/ISO_8601[ISO 8601], e.g. "1985-10-26".
31+
Invalid or unparsable date strings lead to an `ExtensionConfigurationException`.
32+
33+
The `@FailAt` annotation may optionally be declared with a reason to document why the annotated test class or test method fails as soon as the date is reached:
34+
35+
[source,java,indent=0]
36+
----
37+
include::{demo}[tag=fail_at_with_reason]
38+
----
39+
40+
The `@FailAt` annotation can be used on the class and method level, it will be inherited from higher-level containers:
41+
42+
[source,java,indent=0]
43+
----
44+
include::{demo}[tag=fail_at_at_class_level]
45+
----
46+
47+
The `@FailAt` annotation can only be used once per class or method.
48+
49+
== Before and After
50+
51+
The test will be executed normally if the date specified by `date` is in the future, but a warning entry will be published to the https://junit-pioneer.org/docs/report-entries[test report] to indicate that there might be a problem in the future.
52+
53+
If `date` is today or in the past, the test will fail as the execution condition is not fulfilled anymore.
54+
55+
== Thread-Safety
56+
57+
This extension is safe to use during https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution[parallel test execution].
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* http://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junitpioneer.jupiter;
12+
13+
import org.junit.jupiter.api.Nested;
14+
import org.junit.jupiter.api.Test;
15+
16+
public class FailAtExtensionDemo {
17+
18+
// tag::fail_at_simple[]
19+
@Test
20+
@FailAt(date = "2025-01-01")
21+
void test() {
22+
// Test will fail as soon as 1st of January 2025 is reached.
23+
}
24+
// end::fail_at_simple[]
25+
26+
// tag::fail_at_with_reason[]
27+
@Test
28+
@FailAt(reason = "We are not allowed anymore", date = "2025-01-01")
29+
void testWithReason() {
30+
// Test will fail with the given reason as soon as 1st of January 2025 is reached.
31+
}
32+
// end::fail_at_with_reason[]
33+
34+
@Nested
35+
// tag::fail_at_at_class_level[]
36+
@FailAt(date = "2025-01-01")
37+
class TestClass {
38+
39+
@Test
40+
void test() {
41+
// Test will fail as soon as 1st of January 2025 is reached.
42+
}
43+
44+
}
45+
// end::fail_at_at_class_level[]
46+
47+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* http://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junitpioneer.jupiter;
12+
13+
import java.lang.annotation.ElementType;
14+
import java.lang.annotation.Inherited;
15+
import java.lang.annotation.Retention;
16+
import java.lang.annotation.RetentionPolicy;
17+
import java.lang.annotation.Target;
18+
19+
import org.junit.jupiter.api.extension.ExtendWith;
20+
21+
/**
22+
* {@code @FailAt} is a JUnit Jupiter extension to mark tests that shouldn't be executed after a given date,
23+
* essentially failing a test when the date is reached. The date is given as an ISO 8601 string.
24+
*
25+
* <p>It may optionally be declared with a reason to document why the annotated test class or test
26+
* method should fail at the given date.</p>
27+
*
28+
* <p>{@code @FailAt} can be used on the method and class level. It can only be used once per method or class,
29+
* but is inherited from higher-level containers.</p>
30+
*
31+
* <p><strong>WARNING:</strong> This annotation allows the user to move an assumption out of one or multiple test
32+
* method's code into an annotation. But this comes at a cost: Applying {@code @FailAt} can make the test suite
33+
* non-reproducible. If a passing test is run again after the specified date, that build would fail. A report entry is
34+
* issued for every test that does not fail until a certain date.</p>
35+
*
36+
* @since 2.3.0
37+
*/
38+
@Retention(RetentionPolicy.RUNTIME)
39+
@Target({ ElementType.METHOD, ElementType.TYPE })
40+
@Inherited
41+
@ExtendWith(FailAtExtension.class)
42+
public @interface FailAt {
43+
44+
/**
45+
* The reason this annotated test class or test method should fail as soon as the given date is reached.
46+
*/
47+
String reason() default "";
48+
49+
/**
50+
* The date from which this annotated test class or test method should fail as an ISO 8601 string in the
51+
* format yyyy-MM-dd, e.g. 2023-05-28. The test will be executed regularly if that date is not yet reached.
52+
*/
53+
String date();
54+
55+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* http://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junitpioneer.jupiter;
12+
13+
import static java.lang.String.format;
14+
import static org.junit.jupiter.api.extension.ConditionEvaluationResult.enabled;
15+
import static org.junitpioneer.internal.PioneerAnnotationUtils.findClosestEnclosingAnnotation;
16+
17+
import java.time.LocalDate;
18+
import java.time.format.DateTimeFormatter;
19+
import java.time.format.DateTimeParseException;
20+
import java.util.Optional;
21+
22+
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
23+
import org.junit.jupiter.api.extension.ExecutionCondition;
24+
import org.junit.jupiter.api.extension.ExtensionConfigurationException;
25+
import org.junit.jupiter.api.extension.ExtensionContext;
26+
import org.opentest4j.AssertionFailedError;
27+
28+
/**
29+
* This class implements the functionality for the {@code @FailAt} annotation.
30+
*
31+
* @see FailAt
32+
*/
33+
class FailAtExtension implements ExecutionCondition {
34+
35+
private static final DateTimeFormatter ISO_8601 = DateTimeFormatter.ISO_DATE;
36+
37+
@Override
38+
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
39+
return getFailAtDateFromAnnotation(context)
40+
.map(failAtDate -> evaluateFailAtDate(context, failAtDate))
41+
.orElse(enabled("No @FailAt annotation found on element."));
42+
}
43+
44+
private Optional<LocalDate> getFailAtDateFromAnnotation(ExtensionContext context) {
45+
return findClosestEnclosingAnnotation(context, FailAt.class).map(FailAt::date).map(this::parseDate);
46+
}
47+
48+
private LocalDate parseDate(String dateString) {
49+
try {
50+
return LocalDate.parse(dateString, ISO_8601);
51+
}
52+
catch (DateTimeParseException ex) {
53+
throw new ExtensionConfigurationException(
54+
"The `failAtDate` string '" + dateString + "' is not a valid ISO 8601 string.", ex);
55+
}
56+
}
57+
58+
private ConditionEvaluationResult evaluateFailAtDate(ExtensionContext context, LocalDate failAtDate) {
59+
LocalDate today = LocalDate.now();
60+
boolean isBefore = today.isBefore(failAtDate);
61+
62+
String failAtDateString = failAtDate.format(ISO_8601);
63+
String todayDateString = today.format(ISO_8601);
64+
65+
if (isBefore) {
66+
String reportEntry = format(
67+
"The `date` %s is after the current date %s, so `@FailAt` did not fail the test \"%s\". It will do so when the date is reached.",
68+
failAtDateString, todayDateString, context.getUniqueId());
69+
context.publishReportEntry("FailAt", reportEntry);
70+
return enabled(reportEntry);
71+
} else {
72+
String reportEntry = format(
73+
"The current date %s is after or on the `date` %s, so `@FailAt` fails the test \"%s\". Please remove the annotation.",
74+
failAtDateString, todayDateString, context.getUniqueId());
75+
context.publishReportEntry(FailAtExtension.class.getSimpleName(), reportEntry);
76+
77+
String message = format("The current date %s is after or on the `date` %s", todayDateString,
78+
failAtDateString);
79+
80+
throw new AssertionFailedError(message);
81+
}
82+
}
83+
84+
}

src/main/java/org/junitpioneer/jupiter/package-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* <li>{@link org.junitpioneer.jupiter.DisabledUntil}</li>
1111
* <li>{@link org.junitpioneer.jupiter.DisableIfTestFails}</li>
1212
* <li>{@link org.junitpioneer.jupiter.ExpectedToFail}</li>
13+
* <li>{@link org.junitpioneer.jupiter.FailAt}</li>
1314
* <li>{@link org.junitpioneer.jupiter.ReportEntry}</li>
1415
* <li>{@link org.junitpioneer.jupiter.RetryingTest}</li>
1516
* <li>{@link org.junitpioneer.jupiter.StdIo}</li>

0 commit comments

Comments
 (0)