Skip to content

Commit bd9fa0f

Browse files
committed
feat(app-extension): add example JUnit extension consuming BigDataTest context
- Introduced reusable `DummyAppTestExtension` for application tests. - Added `DummyAppTest` annotation for simplified configuration and application lifecycle management. - Created `BigDataJunitContext` for streamlined access to BigDataTestKit in JUnit 5. - Provided a `usage` example showcasing integration of `BigDataTest` and `DummyAppTest`. - Included modular framework components (`dummy-app-framework`, `dummy-app-test-extension`) for custom app configurations and extensions.
1 parent c1f63cb commit bd9fa0f

18 files changed

Lines changed: 336 additions & 4 deletions

File tree

example/app-extension/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# App Extension Example
2+
3+
This example shows how a project-specific JUnit 5 extension can consume the
4+
`bigdata-test` context after `@BigDataTest` starts the kit.
5+
6+
Modules:
7+
8+
- `dummy-app-framework`: a small app framework with `start()` and `stop()`.
9+
- `dummy-app-test-extension`: a reusable JUnit 5 extension that reads
10+
`BigDataJunitContext`, builds app config, starts/stops the app, and injects it
11+
into test methods.
12+
- `usage`: a test module showing how an application test uses `@BigDataTest`,
13+
`@BigDataExtensions`, and `@DummyAppTest` together.
14+
15+
Run it with:
16+
17+
```bash
18+
GRADLE_USER_HOME=/data/.gradle ./gradlew :example:app-extension:usage:test
19+
```
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
plugins {
2+
base
3+
}
4+
5+
description = "Example showing how an application-specific JUnit extension can consume bigdata-test context."
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
plugins {
2+
id("buildsrc.convention.kotlin-jvm")
3+
}
4+
5+
description = "Dummy application framework used by the bigdata-test app extension example"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.openprojectx.bigdata.test.example.app.framework
2+
3+
class DummyApp(
4+
val config: DummyAppConfig,
5+
) {
6+
var started: Boolean = false
7+
private set
8+
9+
fun start() {
10+
started = true
11+
}
12+
13+
fun stop() {
14+
started = false
15+
}
16+
17+
fun property(name: String): String? =
18+
config.properties[name]
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package org.openprojectx.bigdata.test.example.app.framework
2+
3+
data class DummyAppConfig(
4+
val yaml: String,
5+
val properties: Map<String, String>,
6+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.openprojectx.bigdata.test.example.app.framework
2+
3+
object DummyAppFactory {
4+
@JvmStatic
5+
fun create(config: DummyAppConfig): DummyApp =
6+
DummyApp(config)
7+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
plugins {
2+
id("buildsrc.convention.kotlin-jvm")
3+
}
4+
5+
description = "Application-specific JUnit extension consuming bigdata-test context"
6+
7+
dependencies {
8+
api(project(":example:app-extension:dummy-app-framework"))
9+
api(project(":junit5"))
10+
api(project(":extensions"))
11+
api(libs.junitJupiterApi)
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package org.openprojectx.bigdata.test.example.app.extension
2+
3+
import org.openprojectx.bigdata.test.core.BigDataTestKit
4+
import org.openprojectx.bigdata.test.example.app.framework.DummyAppConfig
5+
import org.openprojectx.bigdata.test.extensions.core.BigDataExtensionResult
6+
7+
fun interface DummyAppConfigCustomizer {
8+
fun customize(
9+
kit: BigDataTestKit,
10+
extensionResult: BigDataExtensionResult?,
11+
config: DummyAppConfig,
12+
): DummyAppConfig
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package org.openprojectx.bigdata.test.example.app.extension
2+
3+
import org.junit.jupiter.api.extension.ExtendWith
4+
import kotlin.reflect.KClass
5+
6+
@Target(AnnotationTarget.CLASS)
7+
@Retention(AnnotationRetention.RUNTIME)
8+
@ExtendWith(DummyAppTestExtension::class)
9+
annotation class DummyAppTest(
10+
val autoStart: Boolean = true,
11+
val autoStop: Boolean = true,
12+
val customizer: KClass<out DummyAppConfigCustomizer> = NoopDummyAppConfigCustomizer::class,
13+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package org.openprojectx.bigdata.test.example.app.extension
2+
3+
import org.junit.jupiter.api.Order
4+
import org.junit.jupiter.api.extension.AfterTestExecutionCallback
5+
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback
6+
import org.junit.jupiter.api.extension.ExtensionContext
7+
import org.junit.jupiter.api.extension.ParameterContext
8+
import org.junit.jupiter.api.extension.ParameterResolver
9+
import org.openprojectx.bigdata.test.example.app.framework.DummyApp
10+
import org.openprojectx.bigdata.test.example.app.framework.DummyAppConfig
11+
import org.openprojectx.bigdata.test.example.app.framework.DummyAppFactory
12+
import org.openprojectx.bigdata.test.extensions.junit5.BigDataExtensionResultStore
13+
import org.openprojectx.bigdata.test.junit5.BigDataJunitContext
14+
15+
@Order(200)
16+
class DummyAppTestExtension : BeforeTestExecutionCallback, AfterTestExecutionCallback, ParameterResolver {
17+
override fun beforeTestExecution(context: ExtensionContext) {
18+
val annotation = context.requiredTestClass.getAnnotation(DummyAppTest::class.java)
19+
?: error("@DummyAppTest is missing")
20+
val kit = BigDataJunitContext.getKit(context)
21+
val extensionResult = BigDataExtensionResultStore.getOrNull(context)
22+
val config = customizer(annotation, context).customize(
23+
kit,
24+
extensionResult,
25+
defaultConfig(kit.endpoints().size, kit.springProperties(), extensionResult?.outputs.orEmpty()),
26+
)
27+
val app = DummyAppFactory.create(config)
28+
if (annotation.autoStart) {
29+
app.start()
30+
}
31+
context.store.put(APP_KEY, app)
32+
context.store.put(CONFIG_KEY, config)
33+
}
34+
35+
override fun afterTestExecution(context: ExtensionContext) {
36+
val annotation = context.requiredTestClass.getAnnotation(DummyAppTest::class.java)
37+
?: return
38+
val app = context.store.remove(APP_KEY, DummyApp::class.java) ?: return
39+
if (annotation.autoStop) {
40+
app.stop()
41+
}
42+
context.store.remove(CONFIG_KEY, DummyAppConfig::class.java)
43+
}
44+
45+
override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean =
46+
parameterContext.parameter.type == DummyApp::class.java ||
47+
parameterContext.parameter.type == DummyAppConfig::class.java
48+
49+
override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any =
50+
when (parameterContext.parameter.type) {
51+
DummyApp::class.java -> extensionContext.store.get(APP_KEY, DummyApp::class.java)
52+
?: error("DummyApp has not been created")
53+
DummyAppConfig::class.java -> extensionContext.store.get(CONFIG_KEY, DummyAppConfig::class.java)
54+
?: error("DummyAppConfig has not been created")
55+
else -> error("Unsupported parameter ${parameterContext.parameter}")
56+
}
57+
58+
private fun defaultConfig(
59+
serviceCount: Int,
60+
endpointProperties: Map<String, String>,
61+
extensionOutputs: Map<String, String>,
62+
): DummyAppConfig {
63+
val properties = buildMap {
64+
put("dummy.bigdata.services", serviceCount.toString())
65+
endpointProperties.forEach { (key, value) ->
66+
put("dummy.bigdata.endpoint.$key", value)
67+
}
68+
extensionOutputs.forEach { (key, value) ->
69+
put("dummy.bigdata.extension.$key", value)
70+
}
71+
}
72+
val yaml = buildString {
73+
appendLine("dummy:")
74+
appendLine(" bigdata:")
75+
appendLine(" services: $serviceCount")
76+
if (extensionOutputs.isNotEmpty()) {
77+
appendLine(" extensionOutputs:")
78+
extensionOutputs.forEach { (key, value) ->
79+
appendLine(" $key: \"$value\"")
80+
}
81+
}
82+
}
83+
return DummyAppConfig(yaml = yaml, properties = properties)
84+
}
85+
86+
private fun customizer(
87+
annotation: DummyAppTest,
88+
context: ExtensionContext,
89+
): DummyAppConfigCustomizer =
90+
explicitCustomizer(annotation)
91+
?: companionCustomizer(context)
92+
?: instanceCustomizer(context)
93+
?: NoopDummyAppConfigCustomizer
94+
95+
private fun explicitCustomizer(annotation: DummyAppTest): DummyAppConfigCustomizer? {
96+
val type = annotation.customizer.java
97+
if (type == NoopDummyAppConfigCustomizer::class.java) return null
98+
return type.getDeclaredConstructor().also { it.isAccessible = true }.newInstance()
99+
}
100+
101+
private fun companionCustomizer(context: ExtensionContext): DummyAppConfigCustomizer? =
102+
runCatching {
103+
context.requiredTestClass.getDeclaredField("Companion")
104+
.also { it.isAccessible = true }
105+
.get(null) as? DummyAppConfigCustomizer
106+
}.getOrNull()
107+
108+
private fun instanceCustomizer(context: ExtensionContext): DummyAppConfigCustomizer? =
109+
context.testInstance.orElse(null) as? DummyAppConfigCustomizer
110+
111+
private val ExtensionContext.store: ExtensionContext.Store
112+
get() = getStore(ExtensionContext.Namespace.create(DummyAppTestExtension::class.java, requiredTestClass))
113+
114+
private companion object {
115+
const val APP_KEY = "app"
116+
const val CONFIG_KEY = "config"
117+
}
118+
}

0 commit comments

Comments
 (0)