Skip to content

Commit

Permalink
Introduce @⁠AutoClose
Browse files Browse the repository at this point in the history
This commit introduces an @⁠AutoClose annotation that can be applied to
fields within JUnit Jupiter tests to automatically close the annotated
resource after test execution.

Signed-off-by: Björn Michael <[email protected]>

See #3367
See #3592
  • Loading branch information
bjmi authored and sbrannen committed Jan 4, 2024
1 parent 89ae123 commit e32c2e3
Show file tree
Hide file tree
Showing 9 changed files with 462 additions and 1 deletion.
1 change: 1 addition & 0 deletions documentation/src/docs/asciidoc/link-attributes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ endif::[]
// Jupiter Engine
:junit-jupiter-engine: {javadoc-root}/org.junit.jupiter.engine/org/junit/jupiter/engine/package-summary.html[junit-jupiter-engine]
// Jupiter Extension Implementations
:AutoCloseExtension: {current-branch}/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoCloseExtension.java[AutoCloseExtension]
:DisabledCondition: {current-branch}/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DisabledCondition.java[DisabledCondition]
:RepetitionExtension: {current-branch}/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/RepetitionExtension.java[RepetitionExtension]
:TempDirectory: {current-branch}/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java[TempDirectory]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ repository on GitHub.

==== New Features and Improvements

* ❓
* The new `@AutoClose` annotation can be applied to fields within tests to automatically
close the annotated resource after test execution. See
<<../user-guide/index.adoc#writing-tests-built-in-extensions-AutoClose, User Guide>> for
details.


[[release-notes-5.11.0-M1-junit-vintage]]
Expand Down
24 changes: 24 additions & 0 deletions documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ in the `junit-jupiter-api` module.
| `@ExtendWith` | Used to <<extensions-registration-declarative,register extensions declaratively>>. Such annotations are _inherited_.
| `@RegisterExtension` | Used to <<extensions-registration-programmatic,register extensions programmatically>> via fields. Such fields are _inherited_ unless they are _shadowed_.
| `@TempDir` | Used to supply a <<writing-tests-built-in-extensions-TempDirectory,temporary directory>> via field injection or parameter injection in a lifecycle method or test method; located in the `org.junit.jupiter.api.io` package.
| `@AutoClose` | Denotes that the annotated field represents a resource that should be automatically closed after test execution.
|===

WARNING: Some annotations may currently be _experimental_. Consult the table in
Expand Down Expand Up @@ -2709,3 +2710,26 @@ following precedence rules:
2. The default `TempDirFactory` configured via the configuration
parameter, if present
3. Otherwise, `org.junit.jupiter.api.io.TempDirFactory$Standard` will be used.

[[writing-tests-built-in-extensions-AutoClose]]
==== The AutoClose Extension

The built-in `{AutoCloseExtension}` is used to automatically close resources used in
tests. Therefore, the `@AutoClose` annotation is applied to fields within the
test class to indicate that the annotated resource should be automatically closed after
the test execution.

By default, the `@AutoClose` annotation expects the annotated resource to provide
a `close()` method that will be invoked for closing the resource. However, developers
can customize the closing behavior by providing a different method name through the
`value` attribute. For example, setting `value = "shutdown"` will look
for a method named `shutdown()` to close the resource.

For example, the following test declares a database connection field annotated with
`@AutoClose` that is automatically closed afterward.

[source,java,indent=0]
.A test class using @AutoClose annotation to close used resource
----
include::{testDir}/example/AutoCloseDemo.java[tags=user_guide_example]
----
49 changes: 49 additions & 0 deletions documentation/src/test/java/example/AutoCloseDemo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2015-2023 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
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package example;

import static org.junit.jupiter.api.Assertions.assertTrue;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;

import org.junit.jupiter.api.AutoClose;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

@Disabled
// tag::user_guide_example[]
class AutoCloseDemo {

@AutoClose
Connection connection = getJdbcConnection("jdbc:mysql://localhost/testdb");

@Test
void usersTableHasEntries() throws SQLException {
ResultSet resultSet = connection.createStatement().executeQuery("SELECT * FROM users");

assertTrue(resultSet.next());
}

// ...
// end::user_guide_example[]
private static Connection getJdbcConnection(String url) {
try {
return DriverManager.getConnection(url);
}
catch (SQLException ex) {
throw new RuntimeException(ex);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2015-2023 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
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.jupiter.api;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.apiguardian.api.API;
import org.junit.jupiter.api.extension.ExtendWith;

/**
* The {@code AutoClose} annotation is used to automatically close resources
* used in tests.
*
* <p>This annotation should be applied to fields within test classes. It
* indicates that the annotated resource should be automatically closed after
* the test execution.
*
* <p>By default, the {@code AutoClose} annotation expects the annotated
* resource to provide a {@code close()} method that will be invoked for closing
* the resource. However, developers can customize the closing behavior by
* providing a different method name through the {@link #value} attribute. For
* example, setting {@code value = "shutdown"} will look for a method named
* {@code shutdown()} to close the resource. When multiple annotated resources
* exist the order of closing them is unspecified.
*
* @since 5.11
* @see java.lang.annotation.Retention
* @see java.lang.annotation.Target
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ExtendWith(AutoCloseExtension.class)
@API(status = API.Status.EXPERIMENTAL, since = "5.11")
@SuppressWarnings("exports")
public @interface AutoClose {

/**
* Specifies the name of the method to invoke for closing the resource.
*
* <p>The default value is {@code close}.
*
* @return the method name for closing the resource
*/
String value() default "close";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright 2015-2023 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
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.jupiter.api;

import static org.junit.platform.commons.util.AnnotationUtils.findAnnotatedFields;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.function.Predicate;

import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.ExtensionConfigurationException;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store;
import org.junit.jupiter.api.extension.TestInstancePreDestroyCallback;
import org.junit.platform.commons.logging.Logger;
import org.junit.platform.commons.logging.LoggerFactory;
import org.junit.platform.commons.util.ExceptionUtils;
import org.junit.platform.commons.util.ReflectionUtils;

/**
* {@code AutoCloseExtension} is a JUnit Jupiter extension that closes resources
* if a field in a test class is annotated with {@link AutoClose @AutoClose}.
*
* <p>Consult the Javadoc for {@link AutoClose @AutoClose} for details on the
* contract.
*
* @since 5.11
* @see AutoClose
*/
class AutoCloseExtension implements AfterAllCallback, TestInstancePreDestroyCallback {

private static final Logger logger = LoggerFactory.getLogger(AutoCloseExtension.class);
private static final Namespace NAMESPACE = Namespace.create(AutoClose.class);

@Override
public void afterAll(ExtensionContext context) {
Store contextStore = context.getStore(NAMESPACE);
Class<?> testClass = context.getRequiredTestClass();

registerCloseables(contextStore, testClass, null);
}

@Override
public void preDestroyTestInstance(ExtensionContext context) {
Store contextStore = context.getStore(NAMESPACE);

for (Object instance : context.getRequiredTestInstances().getAllInstances()) {
registerCloseables(contextStore, instance.getClass(), instance);
}
}

private void registerCloseables(Store contextStore, Class<?> testClass, Object testInstance) {
Predicate<Field> predicate = testInstance == null ? ReflectionUtils::isStatic : ReflectionUtils::isNotStatic;
findAnnotatedFields(testClass, AutoClose.class, predicate).forEach(field -> {
try {
contextStore.put(field, asCloseableResource(testInstance, field));
}
catch (Throwable t) {
throw ExceptionUtils.throwAsUncheckedException(t);
}
});
}

private static Store.CloseableResource asCloseableResource(Object testInstance, Field field) {
return () -> {
Object toBeClosed = ReflectionUtils.tryToReadFieldValue(field, testInstance).get();
if (toBeClosed == null) {
logger.warn(() -> "@AutoClose couldn't close object for field " + getQualifiedFieldName(field)
+ " because it was null.");
return;
}
invokeCloseMethod(field, toBeClosed);
};
}

private static void invokeCloseMethod(Field field, Object toBeClosed) {
String methodName = field.getAnnotation(AutoClose.class).value();
Method closeMethod = ReflectionUtils.findMethod(toBeClosed.getClass(), methodName).orElseThrow(
() -> new ExtensionConfigurationException(
"@AutoClose failed to close object for field " + getQualifiedFieldName(field) + " because the "
+ methodName + "() method could not be " + "resolved."));
ReflectionUtils.invokeMethod(closeMethod, toBeClosed);
}

private static String getQualifiedFieldName(Field field) {
return field.getDeclaringClass().getName() + "." + field.getName();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@
exports org.junit.jupiter.api.io;
exports org.junit.jupiter.api.parallel;

opens org.junit.jupiter.api to org.junit.platform.commons;
opens org.junit.jupiter.api.condition to org.junit.platform.commons;
}
Loading

0 comments on commit e32c2e3

Please sign in to comment.