View Javadoc
1   /*
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    * http://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
13   */
14  
15  package de.softwareforge.testing.postgres.junit5;
16  
17  import static com.google.common.base.Preconditions.checkNotNull;
18  import static com.google.common.base.Preconditions.checkState;
19  
20  import de.softwareforge.testing.postgres.embedded.DatabaseInfo;
21  import de.softwareforge.testing.postgres.embedded.DatabaseManager;
22  import de.softwareforge.testing.postgres.embedded.DatabaseManager.DatabaseManagerBuilder;
23  import de.softwareforge.testing.postgres.embedded.EmbeddedPostgres;
24  
25  import jakarta.annotation.Nonnull;
26  import java.lang.reflect.Type;
27  import java.sql.SQLException;
28  import java.util.UUID;
29  import javax.sql.DataSource;
30  
31  import com.google.common.annotations.VisibleForTesting;
32  import org.junit.jupiter.api.extension.AfterAllCallback;
33  import org.junit.jupiter.api.extension.AfterEachCallback;
34  import org.junit.jupiter.api.extension.BeforeAllCallback;
35  import org.junit.jupiter.api.extension.BeforeEachCallback;
36  import org.junit.jupiter.api.extension.ExtensionContext;
37  import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
38  import org.junit.jupiter.api.extension.ExtensionContext.Store;
39  import org.junit.jupiter.api.extension.ParameterContext;
40  import org.junit.jupiter.api.extension.ParameterResolutionException;
41  import org.junit.jupiter.api.extension.ParameterResolver;
42  import org.slf4j.Logger;
43  import org.slf4j.LoggerFactory;
44  
45  /**
46   * A <a href="https://junit.org/junit5/docs/current/user-guide/#extensions">JUnit 5 extension</a> that manages an embedded PostgreSQL database server.
47   * <p>
48   * This extension can provide the {@link EmbeddedPostgres} instance, a {@link DatabaseInfo} or a {@link DataSource} object as test parameters.
49   *
50   * <ul>
51   * <li>Using a {@link DatabaseInfo} parameter is equivalent to calling {@link EmbeddedPgExtension#createDatabaseInfo()}.</li>
52   * <li>Using a {@link DataSource} parameter is equivalent to calling {@link EmbeddedPgExtension#createDataSource()}.</li>
53   * </ul>
54   */
55  public final class EmbeddedPgExtension implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, ParameterResolver {
56  
57      private static final Logger LOG = LoggerFactory.getLogger(EmbeddedPgExtension.class);
58  
59      // multiple instances must use different namespaces
60      private final Namespace pgNamespace = Namespace.create(UUID.randomUUID());
61  
62      private final DatabaseManager.Builder<DatabaseManager> databaseManagerBuilder;
63  
64      private volatile DatabaseManager databaseManager = null;
65  
66      private EmbeddedPgExtension(DatabaseManager.Builder<DatabaseManager> databaseManagerBuilder) {
67          this.databaseManagerBuilder = databaseManagerBuilder;
68      }
69  
70      /**
71       * Creates a new {@link EmbeddedPgExtensionBuilder} that allows further customization of the {@link EmbeddedPgExtension}. The resulting extension manages
72       * the database server in multi-mode (creating multiple databases).
73       *
74       * @return A {@link EmbeddedPgExtensionBuilder} instance. Never null.
75       */
76      @Nonnull
77      static EmbeddedPgExtensionBuilder multiDatabase() {
78          return new EmbeddedPgExtensionBuilder(true);
79      }
80  
81      /**
82       * Creates a new {@link EmbeddedPgExtensionBuilder} that allows further customization of the {@link EmbeddedPgExtension}. The resulting extension manages
83       * the database server in single-mode (using a single database instance).
84       *
85       * @return A {@link EmbeddedPgExtensionBuilder} instance. Never null.
86       */
87      @Nonnull
88      static EmbeddedPgExtensionBuilder singleDatabase() {
89          return new EmbeddedPgExtensionBuilder(false);
90      }
91  
92      /**
93       * Default constructor which allows using this extension with the {@link org.junit.jupiter.api.extension.ExtendWith} annotation.
94       * <p>
95       * This is equivalent to using <pre>{@code
96       *     @RegisterExtension
97       *     public static EmbeddedPgExtension pg = MultiDatabaseBuilder.instanceWithDefaults().build();
98       *     }</pre>
99       *
100      * @since 3.0
101      */
102     public EmbeddedPgExtension() {
103         this(new DatabaseManagerBuilder(true).withInstancePreparer(EmbeddedPostgres.Builder::withDefaults));
104     }
105 
106     /**
107      * 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
108      * call.
109      *
110      * @return A {@link DataSource} instance. This is never null.
111      * @throws SQLException If a problem connecting to the database occurs.
112      */
113     @Nonnull
114     public DataSource createDataSource() throws SQLException {
115         return createDatabaseInfo().asDataSource();
116     }
117 
118     @VisibleForTesting
119     EmbeddedPostgres getEmbeddedPostgres() {
120         return databaseManager.getEmbeddedPostgres();
121     }
122 
123     /**
124      * Returns a new {@link DatabaseInfo} describing a database connection.
125      * <p>
126      * Depending on the mode, this either describes the same database (single mode) or a new database (multi mode).
127      *
128      * @return A {@link DatabaseInfo} instance. This is never null.
129      * @throws SQLException If a problem connecting to the database occurs.
130      */
131     @Nonnull
132     public DatabaseInfo createDatabaseInfo() throws SQLException {
133         checkState(databaseManager != null, "no before method has been called!");
134 
135         DatabaseInfo databaseInfo = databaseManager.getDatabaseInfo();
136         LOG.info("Connection to {}", databaseInfo.asJdbcUrl());
137         return databaseInfo;
138     }
139 
140     @Override
141     public void beforeAll(@Nonnull ExtensionContext extensionContext) throws Exception {
142         checkNotNull(extensionContext, "extensionContext is null");
143 
144         Store pgStore = extensionContext.getStore(pgNamespace);
145 
146         TestingContext testingContext = pgStore.getOrComputeIfAbsent(TestingContext.TESTING_CONTEXT_KEY,
147                 k -> new TestingContext(extensionContext.getUniqueId(), databaseManagerBuilder.build()),
148                 TestingContext.class);
149 
150         this.databaseManager = testingContext.start(extensionContext.getUniqueId());
151     }
152 
153     @Override
154     public void afterAll(@Nonnull ExtensionContext extensionContext) throws Exception {
155         checkNotNull(extensionContext, "extensionContext is null");
156 
157         Store pgStore = extensionContext.getStore(pgNamespace);
158         TestingContext testingContext = pgStore.get(TestingContext.TESTING_CONTEXT_KEY, TestingContext.class);
159 
160         if (testingContext != null) {
161             this.databaseManager = testingContext.stop(extensionContext.getUniqueId());
162         }
163     }
164 
165     @Override
166     public void beforeEach(@Nonnull ExtensionContext extensionContext) throws Exception {
167         checkNotNull(extensionContext, "extensionContext is null");
168 
169         Store pgStore = extensionContext.getStore(pgNamespace);
170         TestingContext testingContext = pgStore.getOrComputeIfAbsent(TestingContext.TESTING_CONTEXT_KEY,
171                 k -> new TestingContext(extensionContext.getUniqueId(), databaseManagerBuilder.build()),
172                 TestingContext.class);
173 
174         this.databaseManager = testingContext.start(extensionContext.getUniqueId());
175     }
176 
177     @Override
178     public void afterEach(@Nonnull ExtensionContext extensionContext) throws Exception {
179         checkNotNull(extensionContext, "extensionContext is null");
180 
181         Store pgStore = extensionContext.getStore(pgNamespace);
182         TestingContext testingContext = pgStore.get(TestingContext.TESTING_CONTEXT_KEY, TestingContext.class);
183 
184         if (testingContext != null) {
185             this.databaseManager = testingContext.stop(extensionContext.getUniqueId());
186         }
187     }
188 
189     @Override
190     public boolean supportsParameter(@Nonnull ParameterContext parameterContext, ExtensionContext extensionContext) {
191         Type type = parameterContext.getParameter().getType();
192         return type == EmbeddedPostgres.class
193                 || type == DatabaseInfo.class
194                 || type == DataSource.class;
195     }
196 
197     @Override
198     public Object resolveParameter(@Nonnull ParameterContext parameterContext, ExtensionContext extensionContext) {
199         Type type = parameterContext.getParameter().getType();
200         try {
201             if (type == EmbeddedPostgres.class) {
202                 return getEmbeddedPostgres();
203             } else if (type == DatabaseInfo.class) {
204                 return createDatabaseInfo();
205 
206             } else if (type == DataSource.class) {
207                 return createDataSource();
208             }
209         } catch (SQLException e) {
210             throw new ParameterResolutionException("Could not create " + type.getTypeName() + " instance", e);
211         }
212         return null;
213     }
214 
215     /**
216      * Builder for {@link EmbeddedPgExtension} customization.
217      */
218     public static final class EmbeddedPgExtensionBuilder extends DatabaseManager.Builder<EmbeddedPgExtension> {
219 
220         private EmbeddedPgExtensionBuilder(boolean multiMode) {
221             super(multiMode);
222         }
223 
224         /**
225          * Create a {@link EmbeddedPgExtension} instance.
226          *
227          * @return A {@link EmbeddedPgExtension} instance. Is never null.
228          */
229         @Override
230         @Nonnull
231         public EmbeddedPgExtension build() {
232             DatabaseManager.Builder<DatabaseManager> databaseManagerBuilder = new DatabaseManagerBuilder(multiMode)
233                     .withDatabasePreparers(databasePreparers.build())
234                     .withInstancePreparers(instancePreparers.build());
235 
236             return new EmbeddedPgExtension(databaseManagerBuilder);
237         }
238     }
239 
240     private static final class TestingContext {
241 
242         private static final Object TESTING_CONTEXT_KEY = new Object();
243 
244         private final String id;
245         private final DatabaseManager databaseManager;
246 
247         private TestingContext(String id, DatabaseManager databaseManager) {
248             this.id = id;
249             this.databaseManager = databaseManager;
250         }
251 
252         private DatabaseManager start(String id) throws Exception {
253             if (this.id.equals(id)) {
254                 databaseManager.start();
255             }
256 
257             return databaseManager;
258         }
259 
260         private DatabaseManager stop(String id) throws Exception {
261             if (this.id.equals(id)) {
262                 databaseManager.close();
263                 return null;
264             }
265 
266             return databaseManager;
267         }
268     }
269 }