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 #createDatabaseInfo()}.</li> 051 * <li>Using a {@link DataSource} parameter is equivalent to calling {@link #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 public EmbeddedPgExtension() { 100 this(new DatabaseManagerBuilder(true).withInstancePreparer(EmbeddedPostgres.Builder::withDefaults)); 101 } 102 103 /** 104 * 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 105 * call. 106 * 107 * @return A {@link DataSource} instance. This is never null. 108 * @throws SQLException If a problem connecting to the database occurs. 109 */ 110 @NonNull 111 public DataSource createDataSource() throws SQLException { 112 return createDatabaseInfo().asDataSource(); 113 } 114 115 @VisibleForTesting 116 EmbeddedPostgres getEmbeddedPostgres() { 117 return databaseManager.getEmbeddedPostgres(); 118 } 119 120 /** 121 * Returns a new {@link DatabaseInfo} describing a database connection. 122 * <p> 123 * Depending on the mode, this either describes the same database (single mode) or a new database (multi mode). 124 * 125 * @return A {@link DatabaseInfo} instance. This is never null. 126 * @throws SQLException If a problem connecting to the database occurs. 127 */ 128 @NonNull 129 public DatabaseInfo createDatabaseInfo() throws SQLException { 130 checkState(databaseManager != null, "no before method has been called!"); 131 132 DatabaseInfo databaseInfo = databaseManager.getDatabaseInfo(); 133 LOG.info("Connection to {}", databaseInfo.asJdbcUrl()); 134 return databaseInfo; 135 } 136 137 @Override 138 public void beforeAll(@NonNull ExtensionContext extensionContext) throws Exception { 139 checkNotNull(extensionContext, "extensionContext is null"); 140 141 Store pgStore = extensionContext.getStore(PG_NAMESPACE); 142 143 TestMode testMode = pgStore.getOrComputeIfAbsent(TestMode.TESTMODE_KEY, 144 k -> new TestMode(extensionContext.getUniqueId(), databaseManagerBuilder.build()), 145 TestMode.class); 146 147 this.databaseManager = testMode.start(extensionContext.getUniqueId()); 148 } 149 150 @Override 151 public void afterAll(@NonNull ExtensionContext extensionContext) throws Exception { 152 checkNotNull(extensionContext, "extensionContext is null"); 153 154 Store pgStore = extensionContext.getStore(PG_NAMESPACE); 155 TestMode testMode = pgStore.get(TestMode.TESTMODE_KEY, TestMode.class); 156 157 if (testMode != null) { 158 this.databaseManager = testMode.stop(extensionContext.getUniqueId()); 159 } 160 } 161 162 @Override 163 public void beforeEach(@NonNull ExtensionContext extensionContext) throws Exception { 164 checkNotNull(extensionContext, "extensionContext is null"); 165 166 Store pgStore = extensionContext.getStore(PG_NAMESPACE); 167 TestMode testMode = pgStore.getOrComputeIfAbsent(TestMode.TESTMODE_KEY, 168 k -> new TestMode(extensionContext.getUniqueId(), databaseManagerBuilder.build()), 169 TestMode.class); 170 171 this.databaseManager = testMode.start(extensionContext.getUniqueId()); 172 } 173 174 @Override 175 public void afterEach(@NonNull ExtensionContext extensionContext) throws Exception { 176 checkNotNull(extensionContext, "extensionContext is null"); 177 178 Store pgStore = extensionContext.getStore(PG_NAMESPACE); 179 TestMode testMode = pgStore.get(TestMode.TESTMODE_KEY, TestMode.class); 180 181 if (testMode != null) { 182 this.databaseManager = testMode.stop(extensionContext.getUniqueId()); 183 } 184 } 185 186 @Override 187 public boolean supportsParameter(@NonNull ParameterContext parameterContext, ExtensionContext extensionContext) { 188 Type type = parameterContext.getParameter().getType(); 189 return type == EmbeddedPostgres.class 190 || type == DatabaseInfo.class 191 || type == DataSource.class; 192 } 193 194 @Override 195 public Object resolveParameter(@NonNull ParameterContext parameterContext, ExtensionContext extensionContext) { 196 Type type = parameterContext.getParameter().getType(); 197 try { 198 if (type == EmbeddedPostgres.class) { 199 return getEmbeddedPostgres(); 200 } else if (type == DatabaseInfo.class) { 201 return createDatabaseInfo(); 202 203 } else if (type == DataSource.class) { 204 return createDataSource(); 205 } 206 } catch (SQLException e) { 207 throw new ParameterResolutionException("Could not create " + type.getTypeName() + " instance", e); 208 } 209 return null; 210 } 211 212 /** 213 * Builder for {@link EmbeddedPgExtension} customization. 214 */ 215 public static final class EmbeddedPgExtensionBuilder extends DatabaseManager.Builder<EmbeddedPgExtension> { 216 217 private EmbeddedPgExtensionBuilder(boolean multiMode) { 218 super(multiMode); 219 } 220 221 /** 222 * Create a {@link EmbeddedPgExtension} instance. 223 * 224 * @return A {@link EmbeddedPgExtension} instance. Is never null. 225 */ 226 @Override 227 @NonNull 228 public EmbeddedPgExtension build() { 229 DatabaseManager.Builder<DatabaseManager> databaseManagerBuilder = new DatabaseManagerBuilder(multiMode) 230 .withDatabasePreparers(databasePreparers.build()) 231 .withInstancePreparers(instancePreparers.build()); 232 233 return new EmbeddedPgExtension(databaseManagerBuilder); 234 } 235 } 236 237 private static final class TestMode { 238 239 private static final Object TESTMODE_KEY = new Object(); 240 241 private final String id; 242 private final DatabaseManager databaseManager; 243 244 private TestMode(String id, DatabaseManager databaseManager) { 245 this.id = id; 246 this.databaseManager = databaseManager; 247 } 248 249 private DatabaseManager start(String id) throws Exception { 250 if (this.id.equals(id)) { 251 databaseManager.start(); 252 } 253 254 return databaseManager; 255 } 256 257 private DatabaseManager stop(String id) throws Exception { 258 if (this.id.equals(id)) { 259 databaseManager.close(); 260 return null; 261 } 262 263 return databaseManager; 264 } 265 } 266}