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  package de.softwareforge.testing.postgres.junit5;
15  
16  import static com.google.common.base.Preconditions.checkNotNull;
17  import static com.google.common.base.Preconditions.checkState;
18  
19  import de.softwareforge.testing.postgres.embedded.DatabaseInfo;
20  import de.softwareforge.testing.postgres.embedded.DatabaseManager;
21  import de.softwareforge.testing.postgres.embedded.DatabaseManager.DatabaseManagerBuilder;
22  import de.softwareforge.testing.postgres.embedded.EmbeddedPostgres;
23  
24  import java.lang.reflect.Type;
25  import java.sql.SQLException;
26  import java.util.UUID;
27  import javax.sql.DataSource;
28  
29  import com.google.common.annotations.VisibleForTesting;
30  import edu.umd.cs.findbugs.annotations.NonNull;
31  import org.junit.jupiter.api.extension.AfterAllCallback;
32  import org.junit.jupiter.api.extension.AfterEachCallback;
33  import org.junit.jupiter.api.extension.BeforeAllCallback;
34  import org.junit.jupiter.api.extension.BeforeEachCallback;
35  import org.junit.jupiter.api.extension.ExtensionContext;
36  import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
37  import org.junit.jupiter.api.extension.ExtensionContext.Store;
38  import org.junit.jupiter.api.extension.ParameterContext;
39  import org.junit.jupiter.api.extension.ParameterResolutionException;
40  import org.junit.jupiter.api.extension.ParameterResolver;
41  import org.slf4j.Logger;
42  import org.slf4j.LoggerFactory;
43  
44  /**
45   * A <a href="https://junit.org/junit5/docs/current/user-guide/#extensions">JUnit 5 extension</a> that manages an embedded PostgreSQL database server.
46   * <p>
47   * This extension can provide the {@link EmbeddedPostgres} instance, a {@link DatabaseInfo} or a {@link DataSource} object as test parameters.
48   *
49   * <ul>
50   * <li>Using a {@link DatabaseInfo} parameter is equivalent to calling {@link #createDatabaseInfo()}.</li>
51   * <li>Using a {@link DataSource} parameter is equivalent to calling {@link #createDataSource()}.</li>
52   * </ul>
53   */
54  public final class EmbeddedPgExtension implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, ParameterResolver {
55  
56      private static final Logger LOG = LoggerFactory.getLogger(EmbeddedPgExtension.class);
57  
58      // multiple instances must use different namespaces
59      private final Namespace PG_NAMESPACE = Namespace.create(UUID.randomUUID());
60  
61      private final DatabaseManager.Builder<DatabaseManager> databaseManagerBuilder;
62  
63      private volatile DatabaseManager databaseManager = null;
64  
65      private EmbeddedPgExtension(DatabaseManager.Builder<DatabaseManager> databaseManagerBuilder) {
66          this.databaseManagerBuilder = databaseManagerBuilder;
67      }
68  
69      /**
70       * Creates a new {@link EmbeddedPgExtensionBuilder} that allows further customization of the {@link EmbeddedPgExtension}. The resulting extension manages
71       * the database server in multi-mode (creating multiple databases).
72       *
73       * @return A {@link EmbeddedPgExtensionBuilder} instance. Never null.
74       */
75      @NonNull
76      static EmbeddedPgExtensionBuilder multiDatabase() {
77          return new EmbeddedPgExtensionBuilder(true);
78      }
79  
80      /**
81       * Creates a new {@link EmbeddedPgExtensionBuilder} that allows further customization of the {@link EmbeddedPgExtension}. The resulting extension manages
82       * the database server in single-mode (using a single database instance).
83       *
84       * @return A {@link EmbeddedPgExtensionBuilder} instance. Never null.
85       */
86      @NonNull
87      static EmbeddedPgExtensionBuilder singleDatabase() {
88          return new EmbeddedPgExtensionBuilder(false);
89      }
90  
91      /**
92       * Default constructor which allows using this extension with the {@link org.junit.jupiter.api.extension.ExtendWith} annotation.
93       * <p>
94       * This is equivalent to using <pre>{@code
95       *     @RegisterExtension
96       *     public static EmbeddedPgExtension pg = MultiDatabaseBuilder.instanceWithDefaults().build();
97       *     }</pre>
98       */
99      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 }