11package io.kotest.extensions.testcontainers
22
3+ import com.zaxxer.hikari.HikariConfig
34import com.zaxxer.hikari.HikariDataSource
45import io.kotest.core.extensions.MountableExtension
56import io.kotest.core.listeners.AfterProjectListener
67import io.kotest.core.listeners.AfterSpecListener
78import io.kotest.core.listeners.AfterTestListener
89import io.kotest.core.listeners.BeforeSpecListener
910import io.kotest.core.listeners.BeforeTestListener
11+ import io.kotest.core.spec.Spec
12+ import io.kotest.core.test.TestCase
13+ import io.kotest.core.test.TestResult
1014import kotlinx.coroutines.Dispatchers
1115import kotlinx.coroutines.withContext
1216import org.testcontainers.containers.JdbcDatabaseContainer
1317
1418/* *
15- * A Kotest [MountableExtension] for [JdbcDatabaseContainer]s that are started the first time they are
16- * installed in a spec.
19+ * A Kotest [MountableExtension] for [JdbcDatabaseContainer]s which is started the first time they are
20+ * installed in a spec. Upon installation, this extension returns an initialized [HikariDataSource]
21+ * connected to the database.
1722 *
1823 * If no spec is executed that installs a particular container,
1924 * then that container is never started.
2025 *
21- * Once mounted in a spec, the return value from the installation point is a configured HikariDataSource.
22- *
2326 * @param container the specific database test container type
2427 * @param beforeSpec a beforeSpec callback
28+ * If the container is never started, this callback will not be invoked.
29+ *
2530 * @param afterSpec an afterSpec callback
31+ * If the container is never started, this callback will not be invoked.
32+ *
2633 * @param beforeTest a beforeTest callback
34+ * If the container is never started, this callback will not be invoked.
35+ *
2736 * @param afterTest a afterTest callback
37+ * If the container is never started, this callback will not be invoked.
2838 *
2939 * @param beforeStart a callback that is invoked only once, just before the container is started.
3040 * If the container is never started, this callback will not be invoked.
@@ -42,48 +52,88 @@ import org.testcontainers.containers.JdbcDatabaseContainer
4252 */
4353class JdbcDatabaseContainerExtension (
4454 private val container : JdbcDatabaseContainer <* >,
45- private val beforeStart : (JdbcDatabaseContainer <* >) -> Unit = {},
46- private val afterStart : (JdbcDatabaseContainer <* >) -> Unit = {},
47- private val beforeShutdown : (JdbcDatabaseContainer <* >) -> Unit = {},
48- private val beforeTest : suspend (HikariDataSource ) -> Unit = {},
49- private val afterTest : suspend (HikariDataSource ) -> Unit = {},
50- private val beforeSpec : suspend (HikariDataSource ) -> Unit = {},
51- private val afterSpec : suspend (HikariDataSource ) -> Unit = {},
52- private val afterShutdown : (HikariDataSource ) -> Unit = {},
53- ) : MountableExtension<TestContainerHikariConfig, HikariDataSource>,
55+ private val mode : ContainerLifecycleMode = ContainerLifecycleMode .Project ,
56+ private val beforeStart : () -> Unit = {},
57+ private val afterStart : () -> Unit = {},
58+ private val beforeTest : suspend (TestCase , HikariDataSource ) -> Unit = { _, _ -> },
59+ private val afterTest : suspend (TestCase , HikariDataSource ) -> Unit = { _, _ -> },
60+ private val beforeSpec : suspend (Spec ) -> Unit = { _ -> },
61+ private val afterSpec : suspend (Spec , HikariDataSource ) -> Unit = { _, _ -> },
62+ private val beforeShutdown : (HikariDataSource ) -> Unit = {},
63+ private val afterShutdown : () -> Unit = { },
64+ ) : MountableExtension<HikariDataSource, HikariDataSource>,
5465 AfterProjectListener ,
5566 BeforeTestListener ,
5667 BeforeSpecListener ,
5768 AfterTestListener ,
5869 AfterSpecListener {
5970
71+ private var dataSource: HikariDataSource ? = null
72+
6073 /* *
6174 * Mounts the container, starting it if necessary. The [configure] block will be invoked
6275 * every time the container is mounted, and after the container has started.
76+ *
77+ * This will return an initialized [HikariDataSource].
78+ * The datasource will be closed in accordance with the provided [ContainerLifecycleMode].
79+ *
80+ * The datasource will be created with default options. If you wish to customize the datasource,
81+ * then ignore this returned value, and instead use the extension function [toDataSource] on the
82+ * container instance.
6383 */
64- override fun mount (configure : TestContainerHikariConfig .() -> Unit ): HikariDataSource {
84+ override fun mount (configure : HikariDataSource .() -> Unit ): HikariDataSource {
6585 if (! container.isRunning) {
66- beforeStart(container )
86+ beforeStart()
6787 container.start()
68- afterStart(container)
88+ dataSource = container.toDataSource()
89+ afterStart()
6990 }
70- return createDataSource(configure)
91+ dataSource?.configure()
92+ return dataSource ? : error(" Datasource not initialized" )
93+ }
94+
95+ override suspend fun beforeTest (testCase : TestCase ) {
96+ beforeTest(testCase, dataSource ? : error(" Datasource not initialized" ))
97+ }
98+
99+ override suspend fun afterTest (testCase : TestCase , result : TestResult ) {
100+ afterTest(testCase, dataSource ? : error(" Datasource not initialized" ))
101+ }
102+
103+ override suspend fun beforeSpec (spec : Spec ) {
104+ beforeSpec(spec)
105+ }
106+
107+ override suspend fun afterSpec (spec : Spec ) {
108+ afterSpec(spec, dataSource ? : error(" Datasource not initialized" ))
109+ if (mode == ContainerLifecycleMode .Spec && container.isRunning) close()
71110 }
72111
73112 override suspend fun afterProject () {
74- if (container.isRunning) withContext(Dispatchers .IO ) {
75- beforeShutdown(container)
113+ if (container.isRunning) close()
114+ }
115+
116+ private suspend fun close () {
117+ withContext(Dispatchers .IO ) {
118+ beforeShutdown(dataSource ? : error(" Datasource not initialized" ))
119+ dataSource?.close()
76120 container.stop()
121+ afterShutdown()
77122 }
78123 }
124+ }
79125
80- private fun createDataSource (configure : TestContainerHikariConfig .() -> Unit ): HikariDataSource {
81- val config = TestContainerHikariConfig ()
82- config.jdbcUrl = container.jdbcUrl
83- config.username = container.username
84- config.password = container.password
85- config.configure()
86- // runInitScripts(ds.connection, config.dbInitScripts)
87- return HikariDataSource (config)
88- }
126+ /* *
127+ * Returns an initialized [HikariDataSource] connected to this [JdbcDatabaseContainer].
128+ *
129+ * @param configure a thunk to configure the [HikariConfig] used to create the datasource.
130+ */
131+ fun JdbcDatabaseContainer <* >.toDataSource (configure : HikariConfig .() -> Unit = {}): HikariDataSource {
132+ val config = HikariConfig ()
133+ config.jdbcUrl = jdbcUrl
134+ config.username = username
135+ config.password = password
136+ config.minimumIdle = 0
137+ config.configure()
138+ return HikariDataSource (config)
89139}
0 commit comments