001/* 002 * Licensed under the Apache License, Version 2.0 (the "License"); 003 * you may not use this file except in compliance with the License. 004 * You may obtain a copy of the License at 005 * 006 * http://www.apache.org/licenses/LICENSE-2.0 007 * 008 * Unless required by applicable law or agreed to in writing, software 009 * distributed under the License is distributed on an "AS IS" BASIS, 010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 011 * See the License for the specific language governing permissions and 012 * limitations under the License. 013 */ 014package de.softwareforge.testing.postgres.junit5; 015 016import static com.google.common.base.Preconditions.checkNotNull; 017import static com.google.common.base.Preconditions.checkState; 018 019import de.softwareforge.testing.postgres.embedded.DatabaseInfo; 020import de.softwareforge.testing.postgres.embedded.DatabaseManager; 021import de.softwareforge.testing.postgres.embedded.DatabaseManager.DatabaseManagerBuilder; 022import de.softwareforge.testing.postgres.embedded.EmbeddedPostgres; 023 024import java.lang.reflect.Type; 025import java.sql.SQLException; 026import java.util.UUID; 027import javax.sql.DataSource; 028 029import com.google.common.annotations.VisibleForTesting; 030import edu.umd.cs.findbugs.annotations.NonNull; 031import org.junit.jupiter.api.extension.AfterAllCallback; 032import org.junit.jupiter.api.extension.AfterEachCallback; 033import org.junit.jupiter.api.extension.BeforeAllCallback; 034import org.junit.jupiter.api.extension.BeforeEachCallback; 035import org.junit.jupiter.api.extension.ExtensionContext; 036import org.junit.jupiter.api.extension.ExtensionContext.Namespace; 037import org.junit.jupiter.api.extension.ExtensionContext.Store; 038import org.junit.jupiter.api.extension.ParameterContext; 039import org.junit.jupiter.api.extension.ParameterResolutionException; 040import org.junit.jupiter.api.extension.ParameterResolver; 041import org.slf4j.Logger; 042import org.slf4j.LoggerFactory; 043 044/** 045 * A <a href="https://junit.org/junit5/docs/current/user-guide/#extensions">JUnit 5 extension</a> that manages an embedded PostgreSQL database server. 046 * <p> 047 * This extension can provide the {@link EmbeddedPostgres} instance, a {@link DatabaseInfo} or a {@link DataSource} object as test parameters. 048 * 049 * <ul> 050 * <li>Using a {@link DatabaseInfo} parameter is equivalent to calling {@link EmbeddedPgExtension#createDatabaseInfo()}.</li> 051 * <li>Using a {@link DataSource} parameter is equivalent to calling {@link EmbeddedPgExtension#createDataSource()}.</li> 052 * </ul> 053 */ 054public final class EmbeddedPgExtension implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, ParameterResolver { 055 056 private static final Logger LOG = LoggerFactory.getLogger(EmbeddedPgExtension.class); 057 058 // multiple instances must use different namespaces 059 private final Namespace PG_NAMESPACE = Namespace.create(UUID.randomUUID()); 060 061 private final DatabaseManager.Builder<DatabaseManager> databaseManagerBuilder; 062 063 private volatile DatabaseManager databaseManager = null; 064 065 private EmbeddedPgExtension(DatabaseManager.Builder<DatabaseManager> databaseManagerBuilder) { 066 this.databaseManagerBuilder = databaseManagerBuilder; 067 } 068 069 /** 070 * Creates a new {@link EmbeddedPgExtensionBuilder} that allows further customization of the {@link EmbeddedPgExtension}. The resulting extension manages 071 * the database server in multi-mode (creating multiple databases). 072 * 073 * @return A {@link EmbeddedPgExtensionBuilder} instance. Never null. 074 */ 075 @NonNull 076 static EmbeddedPgExtensionBuilder multiDatabase() { 077 return new EmbeddedPgExtensionBuilder(true); 078 } 079 080 /** 081 * Creates a new {@link EmbeddedPgExtensionBuilder} that allows further customization of the {@link EmbeddedPgExtension}. The resulting extension manages 082 * the database server in single-mode (using a single database instance). 083 * 084 * @return A {@link EmbeddedPgExtensionBuilder} instance. Never null. 085 */ 086 @NonNull 087 static EmbeddedPgExtensionBuilder singleDatabase() { 088 return new EmbeddedPgExtensionBuilder(false); 089 } 090 091 /** 092 * Default constructor which allows using this extension with the {@link org.junit.jupiter.api.extension.ExtendWith} annotation. 093 * <p> 094 * This is equivalent to using <pre>{@code 095 * @RegisterExtension 096 * public static EmbeddedPgExtension pg = MultiDatabaseBuilder.instanceWithDefaults().build(); 097 * }</pre> 098 * 099 * @since 3.0 100 */ 101 public EmbeddedPgExtension() { 102 this(new DatabaseManagerBuilder(true).withInstancePreparer(EmbeddedPostgres.Builder::withDefaults)); 103 } 104 105 /** 106 * Returns a data source. Depending on the mode it returns a datasource connected to the same database (single mode) or a new database (multi mode) on every 107 * call. 108 * 109 * @return A {@link DataSource} instance. This is never null. 110 * @throws SQLException If a problem connecting to the database occurs. 111 */ 112 @NonNull 113 public DataSource createDataSource() throws SQLException { 114 return createDatabaseInfo().asDataSource(); 115 } 116 117 @VisibleForTesting 118 EmbeddedPostgres getEmbeddedPostgres() { 119 return databaseManager.getEmbeddedPostgres(); 120 } 121 122 /** 123 * Returns a new {@link DatabaseInfo} describing a database connection. 124 * <p> 125 * Depending on the mode, this either describes the same database (single mode) or a new database (multi mode). 126 * 127 * @return A {@link DatabaseInfo} instance. This is never null. 128 * @throws SQLException If a problem connecting to the database occurs. 129 */ 130 @NonNull 131 public DatabaseInfo createDatabaseInfo() throws SQLException { 132 checkState(databaseManager != null, "no before method has been called!"); 133 134 DatabaseInfo databaseInfo = databaseManager.getDatabaseInfo(); 135 LOG.info("Connection to {}", databaseInfo.asJdbcUrl()); 136 return databaseInfo; 137 } 138 139 @Override 140 public void beforeAll(@NonNull ExtensionContext extensionContext) throws Exception { 141 checkNotNull(extensionContext, "extensionContext is null"); 142 143 Store pgStore = extensionContext.getStore(PG_NAMESPACE); 144 145 TestMode testMode = pgStore.getOrComputeIfAbsent(TestMode.TESTMODE_KEY, 146 k -> new TestMode(extensionContext.getUniqueId(), databaseManagerBuilder.build()), 147 TestMode.class); 148 149 this.databaseManager = testMode.start(extensionContext.getUniqueId()); 150 } 151 152 @Override 153 public void afterAll(@NonNull ExtensionContext extensionContext) throws Exception { 154 checkNotNull(extensionContext, "extensionContext is null"); 155 156 Store pgStore = extensionContext.getStore(PG_NAMESPACE); 157 TestMode testMode = pgStore.get(TestMode.TESTMODE_KEY, TestMode.class); 158 159 if (testMode != null) { 160 this.databaseManager = testMode.stop(extensionContext.getUniqueId()); 161 } 162 } 163 164 @Override 165 public void beforeEach(@NonNull ExtensionContext extensionContext) throws Exception { 166 checkNotNull(extensionContext, "extensionContext is null"); 167 168 Store pgStore = extensionContext.getStore(PG_NAMESPACE); 169 TestMode testMode = pgStore.getOrComputeIfAbsent(TestMode.TESTMODE_KEY, 170 k -> new TestMode(extensionContext.getUniqueId(), databaseManagerBuilder.build()), 171 TestMode.class); 172 173 this.databaseManager = testMode.start(extensionContext.getUniqueId()); 174 } 175 176 @Override 177 public void afterEach(@NonNull ExtensionContext extensionContext) throws Exception { 178 checkNotNull(extensionContext, "extensionContext is null"); 179 180 Store pgStore = extensionContext.getStore(PG_NAMESPACE); 181 TestMode testMode = pgStore.get(TestMode.TESTMODE_KEY, TestMode.class); 182 183 if (testMode != null) { 184 this.databaseManager = testMode.stop(extensionContext.getUniqueId()); 185 } 186 } 187 188 @Override 189 public boolean supportsParameter(@NonNull ParameterContext parameterContext, ExtensionContext extensionContext) { 190 Type type = parameterContext.getParameter().getType(); 191 return type == EmbeddedPostgres.class 192 || type == DatabaseInfo.class 193 || type == DataSource.class; 194 } 195 196 @Override 197 public Object resolveParameter(@NonNull ParameterContext parameterContext, ExtensionContext extensionContext) { 198 Type type = parameterContext.getParameter().getType(); 199 try { 200 if (type == EmbeddedPostgres.class) { 201 return getEmbeddedPostgres(); 202 } else if (type == DatabaseInfo.class) { 203 return createDatabaseInfo(); 204 205 } else if (type == DataSource.class) { 206 return createDataSource(); 207 } 208 } catch (SQLException e) { 209 throw new ParameterResolutionException("Could not create " + type.getTypeName() + " instance", e); 210 } 211 return null; 212 } 213 214 /** 215 * Builder for {@link EmbeddedPgExtension} customization. 216 */ 217 public static final class EmbeddedPgExtensionBuilder extends DatabaseManager.Builder<EmbeddedPgExtension> { 218 219 private EmbeddedPgExtensionBuilder(boolean multiMode) { 220 super(multiMode); 221 } 222 223 /** 224 * Create a {@link EmbeddedPgExtension} instance. 225 * 226 * @return A {@link EmbeddedPgExtension} instance. Is never null. 227 */ 228 @Override 229 @NonNull 230 public EmbeddedPgExtension build() { 231 DatabaseManager.Builder<DatabaseManager> databaseManagerBuilder = new DatabaseManagerBuilder(multiMode) 232 .withDatabasePreparers(databasePreparers.build()) 233 .withInstancePreparers(instancePreparers.build()); 234 235 return new EmbeddedPgExtension(databaseManagerBuilder); 236 } 237 } 238 239 private static final class TestMode { 240 241 private static final Object TESTMODE_KEY = new Object(); 242 243 private final String id; 244 private final DatabaseManager databaseManager; 245 246 private TestMode(String id, DatabaseManager databaseManager) { 247 this.id = id; 248 this.databaseManager = databaseManager; 249 } 250 251 private DatabaseManager start(String id) throws Exception { 252 if (this.id.equals(id)) { 253 databaseManager.start(); 254 } 255 256 return databaseManager; 257 } 258 259 private DatabaseManager stop(String id) throws Exception { 260 if (this.id.equals(id)) { 261 databaseManager.close(); 262 return null; 263 } 264 265 return databaseManager; 266 } 267 } 268}