Skip to content

Commit 78a6119

Browse files
committed
Refactoring: Split up JUnit extensions and implement RedisParameterResolver & RedisValidationExtension
1 parent 648921b commit 78a6119

File tree

9 files changed

+276
-24
lines changed

9 files changed

+276
-24
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.github.tobi.laa.spring.boot.embedded.redis.cluster
2+
3+
import io.github.tobi.laa.spring.boot.embedded.redis.junit.extension.RedisFlushAllExtension
4+
import io.github.tobi.laa.spring.boot.embedded.redis.server.RedisServerContextCustomizerFactory
5+
import org.junit.jupiter.api.extension.ExtendWith
6+
import org.springframework.test.context.ContextCustomizerFactories
7+
8+
/**
9+
* Annotation to enable an [embedded Redis cluster][redis.embedded.RedisCluster] for tests.
10+
*/
11+
@Target(AnnotationTarget.CLASS)
12+
@Retention(AnnotationRetention.RUNTIME)
13+
@MustBeDocumented
14+
@ExtendWith(RedisFlushAllExtension::class)
15+
@ContextCustomizerFactories(RedisServerContextCustomizerFactory::class)
16+
annotation class EmbeddedRedisCluster
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.github.tobi.laa.spring.boot.embedded.redis.junit.extension
2+
3+
import org.junit.jupiter.api.extension.ExtendWith
4+
5+
/**
6+
* Compound annotation to enable all JUnit 5 extensions for embedded Redis tests.
7+
*/
8+
@Target(AnnotationTarget.CLASS)
9+
@Retention(AnnotationRetention.RUNTIME)
10+
@ExtendWith(RedisValidationExtension::class)
11+
@ExtendWith(RedisParameterResolver::class)
12+
@ExtendWith(RedisFlushAllExtension::class)
13+
internal annotation class EmbeddedRedisTest

src/main/kotlin/io/github/tobi/laa/spring/boot/embedded/redis/EmbeddedRedisSpringExtension.kt renamed to src/main/kotlin/io/github/tobi/laa/spring/boot/embedded/redis/junit/extension/RedisFlushAllExtension.kt

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,18 @@
1-
package io.github.tobi.laa.spring.boot.embedded.redis
1+
package io.github.tobi.laa.spring.boot.embedded.redis.junit.extension
22

3+
import io.github.tobi.laa.spring.boot.embedded.redis.RedisFlushAll
34
import io.github.tobi.laa.spring.boot.embedded.redis.RedisFlushAll.Mode.AFTER_CLASS
45
import io.github.tobi.laa.spring.boot.embedded.redis.RedisFlushAll.Mode.AFTER_METHOD
5-
import org.junit.jupiter.api.extension.*
6-
import org.springframework.test.context.junit.jupiter.SpringExtension
6+
import io.github.tobi.laa.spring.boot.embedded.redis.RedisStore
7+
import org.junit.jupiter.api.extension.AfterAllCallback
8+
import org.junit.jupiter.api.extension.AfterEachCallback
9+
import org.junit.jupiter.api.extension.ExtensionContext
10+
import org.springframework.test.context.junit.jupiter.SpringExtension.getApplicationContext
711

8-
internal class EmbeddedRedisSpringExtension : BeforeAllCallback, AfterEachCallback, AfterAllCallback,
9-
ParameterResolver {
10-
11-
override fun beforeAll(context: ExtensionContext?) {
12-
// TODO("Not yet implemented")
13-
}
14-
15-
override fun supportsParameter(parameterContext: ParameterContext?, extensionContext: ExtensionContext?): Boolean {
16-
// TODO("Not yet implemented")
17-
return false
18-
}
19-
20-
override fun resolveParameter(parameterContext: ParameterContext?, extensionContext: ExtensionContext?): Any {
21-
// TODO("Not yet implemented")
22-
return Any()
23-
}
12+
/**
13+
* JUnit 5 extension to flush all Redis data after each test method or after all test methods of a test class.
14+
*/
15+
internal class RedisFlushAllExtension : AfterEachCallback, AfterAllCallback {
2416

2517
override fun afterEach(extensionContext: ExtensionContext?) {
2618
if (flushAllMode(extensionContext) == AFTER_METHOD) {
@@ -39,7 +31,7 @@ internal class EmbeddedRedisSpringExtension : BeforeAllCallback, AfterEachCallba
3931
}
4032

4133
private fun flushAll(extensionContext: ExtensionContext?) {
42-
val applicationContext = SpringExtension.getApplicationContext(extensionContext!!)
34+
val applicationContext = getApplicationContext(extensionContext!!)
4335
RedisStore.client(applicationContext)!!.flushAll()
4436
}
4537
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package io.github.tobi.laa.spring.boot.embedded.redis.junit.extension
2+
3+
import io.github.tobi.laa.spring.boot.embedded.redis.RedisStore
4+
import io.github.tobi.laa.spring.boot.embedded.redis.cluster.EmbeddedRedisCluster
5+
import io.github.tobi.laa.spring.boot.embedded.redis.server.EmbeddedRedisServer
6+
import io.github.tobi.laa.spring.boot.embedded.redis.shardedcluster.EmbeddedRedisShardedCluster
7+
import org.junit.jupiter.api.extension.ExtensionContext
8+
import org.junit.jupiter.api.extension.ParameterContext
9+
import org.junit.jupiter.api.extension.ParameterResolver
10+
import org.springframework.test.context.junit.jupiter.SpringExtension.getApplicationContext
11+
import redis.embedded.Redis
12+
import redis.embedded.RedisCluster
13+
import redis.embedded.RedisServer
14+
import redis.embedded.RedisShardedCluster
15+
16+
/**
17+
* JUnit 5 extension to resolve [Redis] parameters.
18+
*/
19+
internal class RedisParameterResolver : ParameterResolver {
20+
21+
override fun supportsParameter(parameterContext: ParameterContext?, extensionContext: ExtensionContext?): Boolean {
22+
val type = parameterType(parameterContext)
23+
return redisResolvable(type)
24+
|| redisServerResolvable(type, extensionContext)
25+
|| clusterResolvable(type, extensionContext)
26+
|| shardedClusterResolvable(type, extensionContext)
27+
}
28+
29+
private fun parameterType(parameterContext: ParameterContext?): Class<*> {
30+
return parameterContext!!.parameter.type
31+
}
32+
33+
private fun redisResolvable(type: Class<*>): Boolean {
34+
return type == Redis::class.java
35+
}
36+
37+
private fun redisServerResolvable(type: Class<*>, extensionContext: ExtensionContext?): Boolean {
38+
return type.isAssignableFrom(RedisServer::class.java)
39+
&& annotatedWith(extensionContext, EmbeddedRedisServer::class.java)
40+
}
41+
42+
private fun clusterResolvable(type: Class<*>, extensionContext: ExtensionContext?): Boolean {
43+
return type.isAssignableFrom(RedisCluster::class.java)
44+
&& annotatedWith(extensionContext, EmbeddedRedisCluster::class.java)
45+
}
46+
47+
private fun shardedClusterResolvable(type: Class<*>, extensionContext: ExtensionContext?): Boolean {
48+
return type.isAssignableFrom(RedisShardedCluster::class.java)
49+
&& annotatedWith(extensionContext, EmbeddedRedisShardedCluster::class.java)
50+
}
51+
52+
private fun annotatedWith(extensionContext: ExtensionContext?, annotationType: Class<out Annotation>): Boolean {
53+
return extensionContext!!.requiredTestClass.isAnnotationPresent(annotationType)
54+
}
55+
56+
override fun resolveParameter(parameterContext: ParameterContext?, extensionContext: ExtensionContext?): Any {
57+
val applicationContext = getApplicationContext(extensionContext!!)
58+
return RedisStore.server(applicationContext)!!
59+
}
60+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package io.github.tobi.laa.spring.boot.embedded.redis.junit.extension
2+
3+
import io.github.tobi.laa.spring.boot.embedded.redis.cluster.EmbeddedRedisCluster
4+
import io.github.tobi.laa.spring.boot.embedded.redis.server.EmbeddedRedisServer
5+
import io.github.tobi.laa.spring.boot.embedded.redis.shardedcluster.EmbeddedRedisShardedCluster
6+
import org.junit.jupiter.api.extension.BeforeAllCallback
7+
import org.junit.jupiter.api.extension.ExtensionContext
8+
9+
/**
10+
* JUnit 5 extension to validate that the API is used correctly.
11+
*/
12+
internal class RedisValidationExtension : BeforeAllCallback {
13+
14+
override fun beforeAll(context: ExtensionContext?) {
15+
val embeddedRedisServer = annotation(context, EmbeddedRedisServer::class.java)
16+
val embeddedRedisCluster = annotation(context, EmbeddedRedisCluster::class.java)
17+
val embeddedRedisShardedCluster = annotation(context, EmbeddedRedisShardedCluster::class.java)
18+
validateMutuallyExclusive(embeddedRedisServer, embeddedRedisCluster, embeddedRedisShardedCluster)
19+
embeddedRedisServer?.let { validateServer(it) }
20+
embeddedRedisCluster?.let { validateCluster(it) }
21+
embeddedRedisShardedCluster?.let { validateShardedCluster(it) }
22+
}
23+
24+
private fun validateMutuallyExclusive(
25+
embeddedRedisServer: EmbeddedRedisServer?,
26+
embeddedRedisCluster: EmbeddedRedisCluster?,
27+
embeddedRedisShardedCluster: EmbeddedRedisShardedCluster?
28+
) {
29+
val count = listOfNotNull(embeddedRedisServer, embeddedRedisCluster, embeddedRedisShardedCluster).count()
30+
require(count <= 1) { "Only one of @EmbeddedRedisServer, @EmbeddedRedisCluster, @EmbeddedRedisShardedCluster is allowed" }
31+
}
32+
33+
private fun validateServer(config: EmbeddedRedisServer) {
34+
validatePort(config.port)
35+
require(config.configFile.isEmpty() || config.settings.isEmpty()) { "Either 'configFile' or 'settings' can be set, but not both" }
36+
require(config.customizer.all { it.constructors.any { it.parameters.isEmpty() } }) { "Customizers must have a no-arg constructor" }
37+
}
38+
39+
private fun validateCluster(config: EmbeddedRedisCluster) {
40+
TODO()
41+
}
42+
43+
private fun validateShardedCluster(config: EmbeddedRedisShardedCluster) {
44+
TODO()
45+
}
46+
47+
private fun validatePort(port: Int) {
48+
require(port in 0..65535) { "Port must be in range 0..65535" }
49+
}
50+
51+
private fun <A : Annotation> annotation(extensionContext: ExtensionContext?, type: Class<A>): A? {
52+
return extensionContext!!.requiredTestClass.getAnnotation(type)
53+
}
54+
}

src/main/kotlin/io/github/tobi/laa/spring/boot/embedded/redis/server/EmbeddedRedisServer.kt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package io.github.tobi.laa.spring.boot.embedded.redis.server
22

3-
import io.github.tobi.laa.spring.boot.embedded.redis.EmbeddedRedisSpringExtension
4-
import org.junit.jupiter.api.extension.ExtendWith
3+
import io.github.tobi.laa.spring.boot.embedded.redis.junit.extension.EmbeddedRedisTest
54
import org.springframework.test.context.ContextCustomizerFactories
65
import kotlin.reflect.KClass
76

@@ -11,7 +10,7 @@ import kotlin.reflect.KClass
1110
@Target(AnnotationTarget.CLASS)
1211
@Retention(AnnotationRetention.RUNTIME)
1312
@MustBeDocumented
14-
@ExtendWith(EmbeddedRedisSpringExtension::class)
13+
@EmbeddedRedisTest
1514
@ContextCustomizerFactories(RedisServerContextCustomizerFactory::class)
1615
annotation class EmbeddedRedisServer(
1716
/**
@@ -22,6 +21,13 @@ annotation class EmbeddedRedisServer(
2221

2322
/**
2423
* The path to the Redis config file to use. If set, the config will be loaded from the file.
24+
*
25+
* > **Warning**
26+
* Due to how embedded Redis is implemented, a [port] specified in the `redis.conf` file will be ignored.
27+
*
28+
* > **Warning**
29+
* Cannot be set together with [settings].
30+
*
2531
* @see redis.embedded.core.RedisServerBuilder.configFile
2632
*/
2733
val configFile: String = "",
@@ -34,6 +40,10 @@ annotation class EmbeddedRedisServer(
3440

3541
/**
3642
* The setting to use. If set, the server will be started with the given settings.
43+
*
44+
* > **Warning**
45+
* Cannot be set together with [configFile].
46+
*
3747
* @see redis.embedded.core.RedisServerBuilder.setting
3848
*/
3949
val settings: Array<String> = [],
Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
11
package io.github.tobi.laa.spring.boot.embedded.redis.shardedcluster
22

3-
annotation class EmbeddedRedisShardedCluster()
3+
import io.github.tobi.laa.spring.boot.embedded.redis.junit.extension.RedisFlushAllExtension
4+
import io.github.tobi.laa.spring.boot.embedded.redis.server.RedisServerContextCustomizerFactory
5+
import org.junit.jupiter.api.extension.ExtendWith
6+
import org.springframework.test.context.ContextCustomizerFactories
7+
8+
/**
9+
* Annotation to enable an [embedded Redis sharded cluster][redis.embedded.RedisShardedCluster] for tests.
10+
*/
11+
@Target(AnnotationTarget.CLASS)
12+
@Retention(AnnotationRetention.RUNTIME)
13+
@MustBeDocumented
14+
@ExtendWith(RedisFlushAllExtension::class)
15+
@ContextCustomizerFactories(RedisServerContextCustomizerFactory::class)
16+
annotation class EmbeddedRedisShardedCluster

src/test/kotlin/io/github/tobi/laa/spring/boot/embedded/redis/RedisTests.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@ import org.assertj.core.api.ObjectAssert
66
import org.springframework.boot.autoconfigure.data.redis.RedisProperties
77
import org.springframework.context.ApplicationContext
88
import org.springframework.context.ApplicationContextAware
9+
import org.springframework.context.annotation.Scope
910
import org.springframework.data.redis.core.RedisTemplate
1011
import org.springframework.stereotype.Component
12+
import redis.embedded.Redis
1113
import java.util.*
1214
import kotlin.random.Random
1315

1416
/**
1517
* Used for sharing common test logic across different test classes.
1618
*/
1719
@Component
20+
@Scope("prototype")
1821
internal open class RedisTests(
1922
val props: RedisProperties,
2023
val template: RedisTemplate<String, Any>,
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package io.github.tobi.laa.spring.boot.embedded.redis.server
2+
3+
import io.github.tobi.laa.spring.boot.embedded.redis.IntegrationTest
4+
import io.github.tobi.laa.spring.boot.embedded.redis.RedisStore
5+
import org.assertj.core.api.Assertions
6+
import org.junit.jupiter.api.*
7+
import org.springframework.context.ApplicationContext
8+
import redis.embedded.Redis
9+
import redis.embedded.RedisInstance
10+
import redis.embedded.RedisServer
11+
12+
@IntegrationTest
13+
@EmbeddedRedisServer
14+
@DisplayName("Parameter resolver test for @EmbeddedRedisServer")
15+
internal class ParamResolverTest {
16+
17+
var paramsFromBeforeEach = listOf<Redis>()
18+
var paramsFromAfterEach = listOf<Redis>()
19+
20+
@Test
21+
@Order(1)
22+
@DisplayName("Should resolve @Test parameters correctly")
23+
fun shouldResolveTestParamsCorrectly(redis: Redis, redisServer: RedisServer, redisInstance: RedisInstance) {
24+
thenParamIsCorrectlyResolved(redis)
25+
thenParamIsCorrectlyResolved(redisServer)
26+
thenParamIsCorrectlyResolved(redisInstance)
27+
}
28+
29+
@BeforeEach
30+
fun beforeEach(redis: Redis, redisServer: RedisServer, redisInstance: RedisInstance) {
31+
paramsFromBeforeEach = listOf(redis, redisServer, redisInstance)
32+
}
33+
34+
@Test
35+
@Order(2)
36+
@DisplayName("Should resolve @BeforeEach params correctly")
37+
fun shouldResolveBeforeEachParamsCorrectly() {
38+
paramsFromBeforeEach.forEach { thenParamIsCorrectlyResolved(it) }
39+
}
40+
41+
@Test
42+
@Order(3)
43+
@DisplayName("Should resolve @BeforeAll params correctly")
44+
fun shouldResolveBeforeAllParamsCorrectly() {
45+
paramsFromBeforeAll.forEach { thenParamIsCorrectlyResolved(it) }
46+
}
47+
48+
@AfterEach
49+
fun afterEach(redis: Redis, redisServer: RedisServer, redisInstance: RedisInstance) {
50+
paramsFromAfterEach = listOf(redis, redisServer, redisInstance)
51+
}
52+
53+
@Test
54+
@Order(4)
55+
@DisplayName("Should resolve @AfterEach params correctly")
56+
fun shouldResolveAfterEachParamsCorrectly() {
57+
paramsFromAfterEach.forEach { thenParamIsCorrectlyResolved(it) }
58+
}
59+
60+
private companion object {
61+
62+
var paramsFromBeforeAll = listOf<Redis>()
63+
64+
var context: ApplicationContext? = null
65+
66+
@JvmStatic
67+
@BeforeAll
68+
fun injectContext(context: ApplicationContext) {
69+
this.context = context
70+
}
71+
72+
@JvmStatic
73+
@BeforeAll
74+
fun beforeAll(redis: Redis, redisServer: RedisServer, redisInstance: RedisInstance) {
75+
paramsFromBeforeAll = listOf(redis, redisServer, redisInstance)
76+
}
77+
78+
@JvmStatic
79+
@AfterAll
80+
fun afterAll(redis: Redis, redisServer: RedisServer, redisInstance: RedisInstance) {
81+
// no @Test methods will be executed after this method, so assertions are put here instead
82+
thenParamIsCorrectlyResolved(redis)
83+
thenParamIsCorrectlyResolved(redisServer)
84+
thenParamIsCorrectlyResolved(redisInstance)
85+
}
86+
87+
private fun thenParamIsCorrectlyResolved(param: Any?) {
88+
Assertions.assertThat(param).isNotNull.isEqualTo(RedisStore.server(context!!)!!)
89+
}
90+
}
91+
}

0 commit comments

Comments
 (0)