Skip to content

Commit

Permalink
GH-3840: Migrate Redis tests to Testcontainers
Browse files Browse the repository at this point in the history
Fixes #3840

* Create a new abstraction `RedisTest` for Testcontainers-based tests
* Move existing common utility methods to the new interface
* Migrate all existing Redis tests to use this new `RedisTest` interface
* Migrate all existing Redis tests to JUnit5
* Make all existing Redis tests confirming to JUnit5 standards, such as
default methods and classes visibility instead of public
* Add a dependency for parametrized tests in JUnit5
* Improve assertions readability across tests, for instance:
assertThat(thing.size()).isEqualTo(2) -> assertThat(thing).hasSize(2)
* Add real assertions to
`RedisLockRegistryTests.twoRedisLockRegistryTest` (earlier it did not
have assertions at all)
* Reformat, rearrange and cleanup the code
* Fix a couple of small changes after the code review:
* Change base interface name to be consistent with other similar places
* Typo in javadocs
* Small tests readability improvement regarding assertions
* Add `opens java.util` to `spring-integration-redis`
 to satisfy a `ReactiveRedisStreamMessageHandlerTests.testMessageWithListPayload()`
 requirements
  • Loading branch information
oxcafedead authored and artembilan committed Jul 15, 2022
1 parent 79d31b2 commit 1c461a3
Show file tree
Hide file tree
Showing 44 changed files with 1,155 additions and 1,372 deletions.
7 changes: 6 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ ext {
springSecurityVersion = project.hasProperty('springSecurityVersion') ? project.springSecurityVersion : '6.0.0-SNAPSHOT'
springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0-SNAPSHOT'
springWsVersion = '4.0.0-SNAPSHOT'
testcontainersVersion = '1.17.1'
testcontainersVersion = '1.17.3'
tomcatVersion = '10.0.21'
xmlUnitVersion = '2.9.0'
xstreamVersion = '1.4.19'
Expand Down Expand Up @@ -260,6 +260,7 @@ configure(javaProjects) { subproject ->
exclude group: 'org.hamcrest'
}
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testImplementation 'org.junit.jupiter:junit-jupiter-params'
testImplementation("com.willowtreeapps.assertk:assertk-jvm:$assertkVersion") {
exclude group: 'org.jetbrains.kotlin'
}
Expand Down Expand Up @@ -839,6 +840,10 @@ project('spring-integration-redis') {
testImplementation "org.hamcrest:hamcrest-core:$hamcrestVersion"
testImplementation 'com.fasterxml.jackson.core:jackson-databind'
}

tasks.withType(JavaForkOptions) {
jvmArgs '--add-opens', 'java.base/java.util=ALL-UNNAMED'
}
}

project('spring-integration-rsocket') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -28,17 +28,18 @@
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.geode.cache.CacheFactory;
import org.junit.Test;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.integration.gemfire.metadata.GemfireMetadataStore;
import org.springframework.integration.jdbc.metadata.JdbcMetadataStore;
import org.springframework.integration.metadata.ConcurrentMetadataStore;
import org.springframework.integration.redis.RedisContainerTest;
import org.springframework.integration.redis.metadata.RedisMetadataStore;
import org.springframework.integration.redis.rules.RedisAvailable;
import org.springframework.integration.redis.rules.RedisAvailableTests;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
Expand All @@ -48,23 +49,30 @@
* @author Gary Russell
* @author Artem Bilan
* @author Bojan Vukasovic
* @author Artem Vozhdayenko
*
* @since 4.0
*
*/
public class PersistentAcceptOnceFileListFilterExternalStoreTests extends RedisAvailableTests {
public class PersistentAcceptOnceFileListFilterExternalStoreTests implements RedisContainerTest {

static RedisConnectionFactory redisConnectionFactory;

@BeforeAll
static void setupConnectionFactory() {
redisConnectionFactory = RedisContainerTest.connectionFactory();
}

@Test
@RedisAvailable
public void testFileSystemWithRedisMetadataStore() throws Exception {
RedisTemplate<String, ?> template = new RedisTemplate<>();
template.setConnectionFactory(this.getConnectionFactoryForTest());
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
template.delete("persistentAcceptOnceFileListFilterRedisTests");

try {
this.testFileSystem(new RedisMetadataStore(this.getConnectionFactoryForTest(),
this.testFileSystem(new RedisMetadataStore(redisConnectionFactory,
"persistentAcceptOnceFileListFilterRedisTests"));
}
finally {
Expand Down Expand Up @@ -95,8 +103,8 @@ public void testFileSystemWithJdbcMetadataStore() throws Exception {
List<Map<String, Object>> metaData = new JdbcTemplate(dataSource)
.queryForList("SELECT * FROM INT_METADATA_STORE");

assertThat(metaData.size()).isEqualTo(1);
assertThat(metaData.get(0).get("METADATA_VALUE")).isEqualTo("43");
assertThat(metaData).hasSize(1);
assertThat(metaData.get(0)).containsEntry("METADATA_VALUE", "43");
}
finally {
dataSource.shutdown();
Expand Down Expand Up @@ -127,28 +135,28 @@ private void testFileSystem(ConcurrentMetadataStore store) throws Exception {
final FileSystemPersistentAcceptOnceFileListFilter filter =
new FileSystemPersistentAcceptOnceFileListFilter(store, "foo:");
final File file = File.createTempFile("foo", ".txt");
assertThat(filter.filterFiles(new File[] { file }).size()).isEqualTo(1);
assertThat(filter.filterFiles(new File[] {file})).hasSize(1);
String ts = store.get("foo:" + file.getAbsolutePath());
assertThat(ts).isEqualTo(String.valueOf(file.lastModified()));
assertThat(filter.filterFiles(new File[] { file }).size()).isEqualTo(0);
file.setLastModified(file.lastModified() + 5000L);
assertThat(filter.filterFiles(new File[] { file }).size()).isEqualTo(1);
assertThat(filter.filterFiles(new File[] {file})).isEmpty();
assertThat(file.setLastModified(file.lastModified() + 5000L)).isTrue();
assertThat(filter.filterFiles(new File[] {file})).hasSize(1);
ts = store.get("foo:" + file.getAbsolutePath());
assertThat(ts).isEqualTo(String.valueOf(file.lastModified()));
assertThat(filter.filterFiles(new File[] { file }).size()).isEqualTo(0);
assertThat(filter.filterFiles(new File[] {file})).isEmpty();

suspend.set(true);
file.setLastModified(file.lastModified() + 5000L);
assertThat(file.setLastModified(file.lastModified() + 5000L)).isTrue();

Future<Integer> result = Executors.newSingleThreadExecutor()
.submit(() -> filter.filterFiles(new File[] { file }).size());
.submit(() -> filter.filterFiles(new File[] {file}).size());
assertThat(latch2.await(10, TimeUnit.SECONDS)).isTrue();
store.put("foo:" + file.getAbsolutePath(), "43");
latch1.countDown();
Integer theResult = result.get(10, TimeUnit.SECONDS);
assertThat(theResult).isEqualTo(Integer.valueOf(0)); // lost the race, key changed

file.delete();
assertThat(file.delete()).isTrue();
filter.close();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,21 @@
* limitations under the License.
*/

package org.springframework.integration.redis.rules;
package org.springframework.integration.redis;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Rule;
import java.time.Duration;

import org.junit.jupiter.api.BeforeAll;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Testcontainers;

import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.BoundListOperations;
import org.springframework.data.redis.core.BoundZSetOperations;
import org.springframework.data.redis.core.RedisTemplate;
Expand All @@ -33,36 +38,64 @@
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;

import io.lettuce.core.ClientOptions;
import io.lettuce.core.SocketOptions;

/**
* The base contract for all tests requiring a Redis connection.
* The Testcontainers 'reuse' option must be disabled, so, Ryuk container is started
* and will clean all the containers up from this test suite after JVM exit.
* Since the Redis container instance is shared via static property, it is going to be
* started only once per JVM, therefore the target Docker container is reused automatically.
*
* @author Artem Vozhdayenko
* @author Oleg Zhurakousky
* @author Gary Russell
* @author Artem Bilan
*
* @since 6.0
*/
public abstract class RedisAvailableTests {
@Testcontainers(disabledWithoutDocker = true)
public interface RedisContainerTest {

@Rule
public RedisAvailableRule redisAvailableRule = new RedisAvailableRule();
GenericContainer<?> REDIS_CONTAINER = new GenericContainer<>("redis:7.0.2")
.withExposedPorts(6379);

@BeforeClass
public static void setupConnectionFactory() {
RedisAvailableRule.setupConnectionFactory();
@BeforeAll
static void startContainer() {
REDIS_CONTAINER.start();
}

@AfterClass
public static void cleanUpConnectionFactoryIfAny() {
RedisAvailableRule.cleanUpConnectionFactoryIfAny();
/**
* A primary method which should be used to connect to the test Redis instance.
* Can be used in any JUnit lifecycle methods if a test class implements this interface.
*/
static LettuceConnectionFactory connectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setPort(REDIS_CONTAINER.getFirstMappedPort());

LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder()
.clientOptions(
ClientOptions.builder()
.socketOptions(
SocketOptions.builder()
.connectTimeout(Duration.ofMillis(10000))
.keepAlive(true)
.build())
.build())
.commandTimeout(Duration.ofSeconds(10000))
.build();

var connectionFactory = new LettuceConnectionFactory(redisStandaloneConfiguration, clientConfiguration);
connectionFactory.afterPropertiesSet();
return connectionFactory;
}

protected RedisConnectionFactory getConnectionFactoryForTest() {
return RedisAvailableRule.connectionFactory;
}

protected void awaitContainerSubscribed(RedisMessageListenerContainer container) throws Exception {
static void awaitContainerSubscribed(RedisMessageListenerContainer container) throws Exception {
awaitContainerSubscribedNoWait(container);
}

private void awaitContainerSubscribedNoWait(RedisMessageListenerContainer container) throws InterruptedException {
static void awaitContainerSubscribedNoWait(RedisMessageListenerContainer container) throws InterruptedException {
RedisConnection connection = null;

int n = 0;
Expand All @@ -82,8 +115,8 @@ private void awaitContainerSubscribedNoWait(RedisMessageListenerContainer contai
assertThat(n < 300).as("RedisMessageListenerContainer Failed to Subscribe").isTrue();
}

protected void awaitContainerSubscribedWithPatterns(RedisMessageListenerContainer container) throws Exception {
this.awaitContainerSubscribed(container);
static void awaitContainerSubscribedWithPatterns(RedisMessageListenerContainer container) throws Exception {
awaitContainerSubscribed(container);
RedisConnection connection = TestUtils.getPropertyValue(container, "subscriber.connection",
RedisConnection.class);

Expand All @@ -96,7 +129,7 @@ protected void awaitContainerSubscribedWithPatterns(RedisMessageListenerContaine
Thread.sleep(1000);
}

protected void awaitFullySubscribed(RedisMessageListenerContainer container, RedisTemplate<?, ?> redisTemplate,
static void awaitFullySubscribed(RedisMessageListenerContainer container, RedisTemplate<?, ?> redisTemplate,
String redisChannelName, QueueChannel channel, Object message) throws Exception {
awaitContainerSubscribedNoWait(container);
drain(channel);
Expand All @@ -110,13 +143,13 @@ protected void awaitFullySubscribed(RedisMessageListenerContainer container, Red
assertThat(received).as("Container failed to fully start").isNotNull();
}

private void drain(QueueChannel channel) {
static void drain(QueueChannel channel) {
while (channel.receive(0) != null) {
// drain
}
}

protected void prepareList(RedisConnectionFactory connectionFactory) {
static void prepareList(RedisConnectionFactory connectionFactory) {

StringRedisTemplate redisTemplate = createStringRedisTemplate(connectionFactory);
redisTemplate.delete("presidents");
Expand All @@ -139,7 +172,7 @@ protected void prepareList(RedisConnectionFactory connectionFactory) {
ops.rightPush("George Washington");
}

protected void prepareZset(RedisConnectionFactory connectionFactory) {
static void prepareZset(RedisConnectionFactory connectionFactory) {

StringRedisTemplate redisTemplate = createStringRedisTemplate(connectionFactory);

Expand All @@ -163,20 +196,19 @@ protected void prepareZset(RedisConnectionFactory connectionFactory) {
ops.add("George Washington", 18);
}

protected void deletePresidents(RedisConnectionFactory connectionFactory) {
this.deleteKey(connectionFactory, "presidents");
static void deletePresidents(RedisConnectionFactory connectionFactory) {
deleteKey(connectionFactory, "presidents");
}

protected void deleteKey(RedisConnectionFactory connectionFactory, String key) {
static void deleteKey(RedisConnectionFactory connectionFactory, String key) {
StringRedisTemplate redisTemplate = createStringRedisTemplate(connectionFactory);
redisTemplate.delete(key);
}

protected StringRedisTemplate createStringRedisTemplate(RedisConnectionFactory connectionFactory) {
static StringRedisTemplate createStringRedisTemplate(RedisConnectionFactory connectionFactory) {
StringRedisTemplate redisTemplate = new StringRedisTemplate();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -26,14 +26,14 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.junit.Test;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.integration.redis.rules.RedisAvailable;
import org.springframework.integration.redis.rules.RedisAvailableTests;
import org.springframework.integration.redis.RedisContainerTest;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.support.GenericMessage;
Expand All @@ -43,22 +43,26 @@
* @author Oleg Zhurakousky
* @author Gary Russell
* @author Artem Bilan
* @author Artem Vozhdayenko
* @since 2.0
*/
public class SubscribableRedisChannelTests extends RedisAvailableTests {
class SubscribableRedisChannelTests implements RedisContainerTest {
private static RedisConnectionFactory redisConnectionFactory;

@BeforeAll
static void setupConnection() {
redisConnectionFactory = RedisContainerTest.connectionFactory();
}

@Test
@RedisAvailable
public void pubSubChannelTest() throws Exception {
RedisConnectionFactory connectionFactory = this.getConnectionFactoryForTest();
void pubSubChannelTest() throws Exception {

SubscribableRedisChannel channel = new SubscribableRedisChannel(connectionFactory, "si.test.channel");
SubscribableRedisChannel channel = new SubscribableRedisChannel(redisConnectionFactory, "si.test.channel");
channel.setBeanFactory(mock(BeanFactory.class));
channel.afterPropertiesSet();
channel.start();

this.awaitContainerSubscribed(TestUtils.getPropertyValue(channel, "container",
RedisContainerTest.awaitContainerSubscribed(TestUtils.getPropertyValue(channel, "container",
RedisMessageListenerContainer.class));

final CountDownLatch latch = new CountDownLatch(3);
Expand All @@ -72,11 +76,9 @@ public void pubSubChannelTest() throws Exception {
}

@Test
@RedisAvailable
public void dispatcherHasNoSubscribersTest() throws Exception {
RedisConnectionFactory connectionFactory = this.getConnectionFactoryForTest();
void dispatcherHasNoSubscribersTest() throws Exception {

SubscribableRedisChannel channel = new SubscribableRedisChannel(connectionFactory, "si.test.channel.no.subs");
SubscribableRedisChannel channel = new SubscribableRedisChannel(redisConnectionFactory, "si.test.channel.no.subs");
channel.setBeanName("dhnsChannel");
channel.setBeanFactory(mock(BeanFactory.class));
channel.afterPropertiesSet();
Expand Down
Loading

0 comments on commit 1c461a3

Please sign in to comment.