Skip to content

Commit 6d00773

Browse files
authored
Use Testcontainers for integration tests (#2033)
* Use Testcontainers for integration tests Signed-off-by: sanghun <vitash1215@gmail.com> * Remove redundant Testcontainers startup call Signed-off-by: sanghun <vitash1215@gmail.com> * Handle unknown OpenSearch versions conservatively Signed-off-by: sanghun <vitash1215@gmail.com> * Use official OpenSearch Testcontainers Signed-off-by: sanghun <vitash1215@gmail.com> * Wire the test container through a JUnit 4 class rule Replace the static helper with an ExternalResource under src/test/java21, use the suggested testImplementation dependencies, and drop the local image-name tests. Keep the thread-leak filter as agreed in review. Signed-off-by: sanghun <vitash1215@gmail.com> * Make the container rule fields private and drop synchronized The constants are only used within the rule, and JUnit 4 initializes the class rule on a single thread per JVM fork, so the lock guards nothing. Signed-off-by: sanghun <vitash1215@gmail.com> --------- Signed-off-by: sanghun <vitash1215@gmail.com>
1 parent 0707347 commit 6d00773

8 files changed

Lines changed: 157 additions & 33 deletions

File tree

.github/workflows/test-integration-unreleased.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ jobs:
9090
- name: Run Integration Test
9191
run: |
9292
cd opensearch-java
93-
./gradlew clean integrationTest -Dhttps=false
93+
./gradlew clean integrationTest -Dhttps=false -Dtests.opensearch.testcontainers.enabled=false
9494
9595
- name: Upload Reports
9696
if: failure()

.github/workflows/test-integration.yml

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -40,27 +40,8 @@ jobs:
4040
java-version: ${{ matrix.entry.java }}
4141
distribution: 'temurin'
4242
cache: 'gradle'
43-
- name: Run Docker
44-
run: |
45-
echo "PASSWORD=admin" >> $GITHUB_ENV
46-
docker info
47-
docker compose --project-directory .ci/opensearch build --build-arg OPENSEARCH_VERSION=${{ matrix.entry.opensearch_version }}
48-
docker compose --project-directory .ci/opensearch up -d
49-
sleep 60
50-
51-
- name: Sets password (new versions)
52-
run: |
53-
VERSION_COMPONENTS=(${OPENSEARCH_VERSION//./ })
54-
MAJOR_VERSION=${VERSION_COMPONENTS[0]}
55-
MINOR_VERSION=${VERSION_COMPONENTS[1]}
56-
if (( $MAJOR_VERSION > 2 || ( $MAJOR_VERSION == 2 && $MINOR_VERSION >= 12 ) )); then
57-
echo "PASSWORD=0_aD^min_0" >> $GITHUB_ENV
58-
fi
59-
env:
60-
OPENSEARCH_VERSION: ${{ matrix.entry.opensearch_version }}
61-
6243
- name: Run Integration Test
63-
run: ./gradlew clean integrationTest -Dpassword=${{ env.PASSWORD }}
44+
run: ./gradlew clean integrationTest -Dtests.opensearch.version=${{ matrix.entry.opensearch_version }}
6445

6546
- name: Upload Reports
6647
if: failure()
@@ -69,7 +50,3 @@ jobs:
6950
name: test-reports-os${{ matrix.entry.opensearch_version }}-java${{ matrix.entry.java }}
7051
path: java-client/build/reports/
7152
retention-days: 7
72-
73-
- name: Stop Docker
74-
run: |
75-
docker compose --project-directory .ci/opensearch down

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
1717
- Bump `org.apache.httpcomponents.client5:httpclient5` from 5.6 to 5.6.1 ([#1967](https://github.com/opensearch-project/opensearch-java/pull/1967))
1818

1919
### Added
20+
- Run Java client integration tests with a Testcontainers-managed OpenSearch instance by default ([#2033](https://github.com/opensearch-project/opensearch-java/pull/2033))
2021
- Detect AWS SDK `Apache5HttpClient` in `AwsSdk2Transport` body-method guardrail ([#1903](https://github.com/opensearch-project/opensearch-java/pull/1970))
2122
- Support Jackson 3.x release line ([#1810](https://github.com/opensearch-project/opensearch-java/pull/1810))
2223
- Added `equals()` and `hashCode()` implementations to `FieldValue` ([#1998](https://github.com/opensearch-project/opensearch-java/pull/1998))

DEVELOPER_GUIDE.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,28 @@ To run unit tests for the java-client:
5252

5353
#### Integration Tests
5454

55-
To run integration tests for the java-client, start an OpenSearch cluster using docker and pass the OpenSearch version:
55+
To run integration tests for the java-client:
5656

5757
```
58-
docker-compose --project-directory .ci/opensearch build --build-arg OPENSEARCH_VERSION=1.3.0
59-
docker-compose --project-directory .ci/opensearch up -d
58+
./gradlew clean integrationTest
6059
```
6160

62-
Run integration tests after starting OpenSearch cluster:
61+
By default, the integration test task starts a single OpenSearch test container for the test JVM. To test against a specific OpenSearch image version, pass the OpenSearch version:
6362

6463
```
65-
./gradlew clean integrationTest
64+
./gradlew clean integrationTest -Dtests.opensearch.version=3.2.0
65+
```
66+
67+
To pass the full official OpenSearch image name, use:
68+
69+
```
70+
./gradlew clean integrationTest -Dtests.opensearch.image=opensearchproject/opensearch:3.2.0
71+
```
72+
73+
To run against an already running cluster, disable the test container and pass the cluster endpoint if it is not `localhost:9200`:
74+
75+
```
76+
./gradlew clean integrationTest -Dtests.opensearch.testcontainers.enabled=false -Dtests.rest.cluster=localhost:9200
6677
```
6778

6879
#### AWS Transport Integration Tests

java-client/build.gradle.kts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ tasks.build {
143143
dependsOn("spotlessJavaCheck")
144144
}
145145

146+
val opensearchVersion = "3.5.0-SNAPSHOT"
147+
val opensearchDockerVersion = opensearchVersion.removeSuffix("-SNAPSHOT")
148+
146149
tasks.test {
147150
systemProperty("tests.security.manager", "false")
148151

@@ -168,6 +171,16 @@ val integrationTest = task<Test>("integrationTest") {
168171
systemProperty("https", System.getProperty("https", "true"))
169172
systemProperty("user", System.getProperty("user", "admin"))
170173
systemProperty("password", System.getProperty("password", "admin"))
174+
systemProperty(
175+
"tests.opensearch.testcontainers.enabled",
176+
System.getProperty("tests.opensearch.testcontainers.enabled", "true")
177+
)
178+
systemProperty(
179+
"tests.opensearch.version",
180+
System.getProperty("tests.opensearch.version", opensearchDockerVersion)
181+
)
182+
System.getProperty("tests.rest.cluster")?.let { systemProperty("tests.rest.cluster", it) }
183+
System.getProperty("tests.opensearch.image")?.let { systemProperty("tests.opensearch.image", it) }
171184
systemProperty("tests.awsSdk2support.domainHost",
172185
System.getProperty("tests.awsSdk2support.domainHost", null))
173186
systemProperty("tests.awsSdk2support.serviceName",
@@ -176,8 +189,6 @@ val integrationTest = task<Test>("integrationTest") {
176189
System.getProperty("tests.awsSdk2support.domainRegion", "us-east-1"))
177190
}
178191

179-
val opensearchVersion = "3.5.0-SNAPSHOT"
180-
181192
dependencies {
182193
val jacksonVersion = "2.21.2"
183194
val jacksonDatabindVersion = "2.21.2"
@@ -377,6 +388,7 @@ if (runtimeJavaVersion >= JavaVersion.VERSION_21) {
377388
compileClasspath += sourceSets.main.get().output + sourceSets.test.get().output
378389
runtimeClasspath += sourceSets.main.get().output + sourceSets.test.get().output
379390
srcDir("src/test/java11")
391+
srcDir("src/test/java21")
380392
}
381393
}
382394

@@ -387,6 +399,8 @@ if (runtimeJavaVersion >= JavaVersion.VERSION_21) {
387399
testImplementation("org.opensearch.test", "framework", opensearchVersion) {
388400
exclude(group = "org.hamcrest")
389401
}
402+
testImplementation("org.opensearch:opensearch-testcontainers:4.1.0")
403+
testImplementation("org.testcontainers:testcontainers:2.0.4")
390404
}
391405

392406
tasks.named<JavaCompile>("compileJava21Java") {
@@ -408,4 +422,4 @@ if (runtimeJavaVersion >= JavaVersion.VERSION_21) {
408422
testClassesDirs += java21.output.classesDirs
409423
classpath = sourceSets["java21"].runtimeClasspath
410424
}
411-
}
425+
}

java-client/src/test/java11/org/opensearch/client/opensearch/integTest/OpenSearchJavaClientTestCase.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
package org.opensearch.client.opensearch.integTest;
1010

11+
import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters;
1112
import java.io.IOException;
1213
import java.util.ArrayList;
1314
import java.util.Collections;
@@ -30,6 +31,7 @@
3031
import org.junit.After;
3132
import org.junit.AfterClass;
3233
import org.junit.Before;
34+
import org.junit.ClassRule;
3335
import org.opensearch.Version;
3436
import org.opensearch.client.RestClient;
3537
import org.opensearch.client.RestClientBuilder;
@@ -45,6 +47,7 @@
4547
import org.opensearch.common.settings.Settings;
4648
import org.opensearch.test.rest.OpenSearchRestTestCase;
4749

50+
@ThreadLeakFilters(filters = TestcontainersThreadFilter.class)
4851
public abstract class OpenSearchJavaClientTestCase extends OpenSearchRestTestCase implements OpenSearchTransportSupport {
4952
private static final List<String> systemIndices = List.of(
5053
".opensearch-observability",
@@ -59,6 +62,11 @@ public abstract class OpenSearchJavaClientTestCase extends OpenSearchRestTestCas
5962
private static TreeSet<Version> nodeVersions;
6063
private static List<HttpHost> clusterHosts;
6164

65+
// The integration tests run through JUnit 4 (RandomizedRunner), so @ClassRule is the pre/post
66+
// lifecycle hook; the rule starts a single container shared by the whole test JVM.
67+
@ClassRule
68+
public static final OpenSearchTestContainerRule testContainer = new OpenSearchTestContainerRule();
69+
6270
@Before
6371
public void initJavaClient() throws IOException {
6472
if (javaClient == null) {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.client.opensearch.integTest;
10+
11+
import com.carrotsearch.randomizedtesting.ThreadFilter;
12+
13+
public final class TestcontainersThreadFilter implements ThreadFilter {
14+
@Override
15+
public boolean reject(Thread thread) {
16+
String name = thread.getName();
17+
// Testcontainers owns these helper threads and Ryuk cleans them up after the JVM exits.
18+
return "testcontainers-ryuk".equals(name) || name.startsWith("testcontainers-pull-watchdog-") || name.startsWith("ducttape-");
19+
}
20+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.client.opensearch.integTest;
10+
11+
import java.net.URI;
12+
import org.junit.rules.ExternalResource;
13+
import org.opensearch.testcontainers.OpenSearchContainer;
14+
import org.opensearch.testcontainers.OpenSearchDockerImage;
15+
16+
/**
17+
* Starts one OpenSearch test container for the whole test JVM and points the integration tests at
18+
* it through system properties. The container is shared by every test class, so it is left running
19+
* after each class; Testcontainers' Ryuk reaper removes it once the JVM exits.
20+
*/
21+
final class OpenSearchTestContainerRule extends ExternalResource {
22+
private static final String ENABLED_PROPERTY = "tests.opensearch.testcontainers.enabled";
23+
private static final String VERSION_PROPERTY = "tests.opensearch.version";
24+
private static final String IMAGE_PROPERTY = "tests.opensearch.image";
25+
private static final String CLUSTER_PROPERTY = "tests.rest.cluster";
26+
private static final String HTTPS_PROPERTY = "https";
27+
private static final String USER_PROPERTY = "user";
28+
private static final String PASSWORD_PROPERTY = "password";
29+
30+
private static final String DEFAULT_ADMIN_PASSWORD = "admin";
31+
32+
private static OpenSearchContainer<?> container;
33+
34+
@Override
35+
protected void before() {
36+
startIfNeeded();
37+
}
38+
39+
private static void startIfNeeded() {
40+
if (hasText(System.getProperty(CLUSTER_PROPERTY)) || !testcontainersEnabled()) {
41+
return;
42+
}
43+
44+
if (container == null) {
45+
OpenSearchContainer<?> openSearch = createContainer();
46+
openSearch.start();
47+
container = openSearch;
48+
}
49+
50+
// getHttpHostAddress() is scheme-prefixed, but tests.rest.cluster expects host:port.
51+
URI httpHostAddress = URI.create(container.getHttpHostAddress());
52+
System.setProperty(CLUSTER_PROPERTY, httpHostAddress.getHost() + ":" + httpHostAddress.getPort());
53+
System.setProperty(HTTPS_PROPERTY, Boolean.toString(container.isSecurityEnabled()));
54+
System.setProperty(USER_PROPERTY, container.getUsername());
55+
System.setProperty(PASSWORD_PROPERTY, container.getPassword());
56+
}
57+
58+
private static OpenSearchContainer<?> createContainer() {
59+
String image = System.getProperty(IMAGE_PROPERTY);
60+
OpenSearchContainer<?> openSearch = hasText(image)
61+
? new OpenSearchContainer<>(image)
62+
: new OpenSearchContainer<>(OpenSearchDockerImage.ofVersion(requiredVersion()));
63+
64+
// Disk watermarks must stay disabled; constrained disks otherwise trip index_create_block_exception.
65+
openSearch.withSecurityEnabled().withEnv("cluster.routing.allocation.disk.threshold_enabled", "false");
66+
67+
// The container has no password setter; OPENSEARCH_INITIAL_ADMIN_PASSWORD is its supported
68+
// input and getPassword() reflects it. Only forward a non-default override: when the env is
69+
// unset, the container substitutes its own strong default on images >= 2.12.
70+
String configuredPassword = System.getProperty(PASSWORD_PROPERTY);
71+
if (hasText(configuredPassword) && !DEFAULT_ADMIN_PASSWORD.equals(configuredPassword)) {
72+
openSearch.withEnv("OPENSEARCH_INITIAL_ADMIN_PASSWORD", configuredPassword);
73+
}
74+
75+
return openSearch;
76+
}
77+
78+
private static String requiredVersion() {
79+
String version = System.getProperty(VERSION_PROPERTY);
80+
if (!hasText(version)) {
81+
throw new IllegalStateException("Missing " + VERSION_PROPERTY + " for OpenSearch Testcontainers image");
82+
}
83+
return version;
84+
}
85+
86+
private static boolean testcontainersEnabled() {
87+
return Boolean.parseBoolean(System.getProperty(ENABLED_PROPERTY, "true"));
88+
}
89+
90+
private static boolean hasText(String value) {
91+
return value != null && !value.isBlank();
92+
}
93+
}

0 commit comments

Comments
 (0)