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

Introduce @AutoClose #3592

Closed
wants to merge 11 commits into from
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.
sbrannen marked this conversation as resolved.
Show resolved Hide resolved
|===

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;

/**
bjmi marked this conversation as resolved.
Show resolved Hide resolved
* 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 {
bjmi marked this conversation as resolved.
Show resolved Hide resolved
bjmi marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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;
sbrannen marked this conversation as resolved.
Show resolved Hide resolved

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()) {
sbrannen marked this conversation as resolved.
Show resolved Hide resolved
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 -> {
sbrannen marked this conversation as resolved.
Show resolved Hide resolved
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.");
sbrannen marked this conversation as resolved.
Show resolved Hide resolved
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
Loading