org.kiwiproject.test.junit.jupiter.H2FileBasedDatabaseExtension Maven / Gradle / Ivy
package org.kiwiproject.test.junit.jupiter;
import static java.util.Comparator.reverseOrder;
import static java.util.Objects.nonNull;
import static org.kiwiproject.test.junit.jupiter.JupiterHelpers.isTestClassNested;
import static org.kiwiproject.test.junit.jupiter.JupiterHelpers.testClassNameOrNull;
import com.google.common.annotations.VisibleForTesting;
import lombok.Getter;
import lombok.experimental.Delegate;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.kiwiproject.test.h2.H2DatabaseTestHelper;
import org.kiwiproject.test.h2.H2FileBasedDatabase;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* JUnit Jupiter extension that creates a file-based H2 database before all tests and deletes it after all tests
* have executed. It also provides for injection of the database into test lifecycle methods that declare
* a {@link H2FileBasedDatabase} annotated with {@link H2Database}.
*
* You can register the extension via {@code ExtendWith} and then use parameter resolution with the {@link H2Database}
* annotation to obtain the {@link H2FileBasedDatabase} instance in a lifecycle or test method. Example:
*
* {@literal @}ExtendWith(H2FileBasedDatabaseExtension.class)
* class MyFirstTest {
*
* {@literal @}BeforeEach
* void setUp(@H2Database H2FileBasedDatabase database) { ... }
*
* {@literal @}Test
* void shouldDoSomethingWithTheDatabase(@H2Database H2FileBasedDatabase database) { ... }
* }
*
*
* Alternatively, if you need to supply another extension with the H2 database, then you can register the extension
* on a static field using {@code @RegisterExtension}. Here is an example using a theoretical extension named
* {@code DaoExtension} that requires a {@code DataSource}. The data source is obtained from the
* {@link H2FileBasedDatabase} provided by this extension:
*
* class MySecondTest {
*
* {@literal @}RegisterExtension
* static final H2FileBasedDatabaseExtension DATABASE_EXTENSION = new H2FileBasedDatabaseExtension();
*
* {@literal @}RegisterExtension
* final DaoExtension<PersonDao> jdbi3DaoExtension =
* DaoExtension.<PersonDao>builder()
* .daoType(PersonDao.class)
* .dataSource(DATABASE_EXTENSION.getDataSource()) // supply the DataSource here
* .plugin(new H2DatabasePlugin())
* .build();
* }
*
* NOTE:The second example only works when the extension that requires the {@link H2FileBasedDatabase}
* is an instance field. If it is static, it will not work!
*
* When using this extension via {@code @RegisterExtension}, you can use the getter methods to retrieve the entire
* {@link H2FileBasedDatabase} object or its individual properties. When using it with {@code @ExtendWith} and an
* injected parameter, you obviously have direct access to the {@link H2FileBasedDatabase} object.
*
* This requires h2 and
* commons-io to be
* available at runtime when tests are executing.
*/
@Slf4j
public class H2FileBasedDatabaseExtension implements BeforeAllCallback, AfterAllCallback, ParameterResolver {
private static final String DATABASE_KEY = "database";
@Getter
@Delegate
private H2FileBasedDatabase database;
/**
* Creates a new H2 file-based database if a database does not exist.
*
* @param context extension context
*/
@Override
public void beforeAll(ExtensionContext context) {
if (nonNull(database)) {
LOG.trace("A database already exists (we are probably inside a @Nested test class) so not doing anything");
return;
}
LOG.trace("Database does not exist; create it");
database = H2DatabaseTestHelper.buildH2FileBasedDatabase();
var namespace = createNamespace();
context.getStore(namespace).put(DATABASE_KEY, database);
LOG.trace("Created and stored database: {}", database);
}
/**
* Deletes the H2 file-based database unless exiting a nested test class.
*
* @param context extension context
* @throws Exception if the database directory could not be deleted
*/
@Override
public void afterAll(ExtensionContext context) throws Exception {
if (isTestClassNested(context)) {
LOG.trace("We're in nested class {}, so NOT deleting the database", testClassNameOrNull(context));
} else {
LOG.trace("Deleting database: {}", database);
deleteDirectory(database.getDirectory());
}
}
private static void deleteDirectory(File directory) throws IOException {
var pathToBeDeleted = directory.toPath();
try (var pathStream = Files.walk(pathToBeDeleted)) {
pathStream.sorted(reverseOrder()).forEach(H2FileBasedDatabaseExtension::deleteOrThrowUnchecked);
}
}
@VisibleForTesting
static void deleteOrThrowUnchecked(Path p) {
try {
Files.delete(p);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Does the parameter need to be resolved?
*
* @param parameterContext parameter context
* @param extensionContext extension context
* @return true if the {@code parameterContext} is annotated with {@link H2Database}
*/
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return parameterContext.isAnnotated(H2Database.class);
}
/**
* Resolve the parameter annotated with {@link H2Database} into a {@link H2FileBasedDatabase}.
*
* @param parameterContext parameter context
* @param extensionContext extension context
* @return the resolved {@link H2FileBasedDatabase}
*/
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
var namespace = createNamespace();
return getDatabase(extensionContext, namespace);
}
private ExtensionContext.Namespace createNamespace() {
return ExtensionContext.Namespace.create(getClass(), "H2FileBasedDatabase", Thread.currentThread().getName());
}
private H2FileBasedDatabase getDatabase(ExtensionContext context, ExtensionContext.Namespace namespace) {
return context.getStore(namespace).get(DATABASE_KEY, H2FileBasedDatabase.class);
}
}