Skip to content

chore(internal): set up TestContainers for running mock server #537

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 23 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0ad3200
add draft contributing.md
dtmeadows Jun 27, 2025
d063ba7
remove a few things
dtmeadows Jun 27, 2025
88f4f5a
Apply suggestions from code review
dtmeadows Jul 2, 2025
7897cce
Merge branch 'next' into dmeadows/java-contributing-md
dtmeadows Jul 15, 2025
a9752cf
address more comments
dtmeadows Jul 15, 2025
66d80ec
Merge branch 'dmeadows/java-contributing-md' of https://github.com/st…
dtmeadows Jul 15, 2025
39d98f6
Set up TestContainers instead of running the mock server from a script.
jdubois Jul 16, 2025
3786a1e
Update CONTRIBUTING.md
dtmeadows Jul 16, 2025
abe671e
Apply suggestions from code review
dtmeadows Jul 16, 2025
1905703
pr comments
dtmeadows Jul 16, 2025
608947c
chore(internal): Add CONTRIBUTING.md for SDK developers
dtmeadows Jul 16, 2025
3239c2d
chore(internal): allow running specific example from cli
stainless-app[bot] Jul 16, 2025
a00c39b
fix(client): ensure error handling always occurs
stainless-app[bot] Jul 17, 2025
77f54fd
feat(client): add `ResponseAccumulator` (#391)
damo Jul 17, 2025
2d185ba
chore(client): remove non-existent method
TomerAberbach Jul 17, 2025
6fa0700
Set up TestContainers instead of running the mock server from a script.
jdubois Jul 16, 2025
32e22a4
Merge remote-tracking branch 'origin/fix-54' into fix-54
jdubois Jul 18, 2025
d97e27d
Use Stainless version of Prism
jdubois Jul 18, 2025
1f5bb24
Merge branch 'next' into fix-54
TomerAberbach Jul 18, 2025
5e02837
chore: format
TomerAberbach Jul 18, 2025
fdeac0b
chore: keep ./scripts/test
TomerAberbach Jul 18, 2025
dd9c8c1
chore: remove dupe test impls
TomerAberbach Jul 18, 2025
ac635a9
chore: get rid of ./scripts/mock
TomerAberbach Jul 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .github/workflows/create-releases.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,6 @@ jobs:
run: |
./gradlew :openai-java-core:compileJava :openai-java-core:compileTestJava -x test

- name: Run the Prism server
run: |
./scripts/mock --daemon

- name: Setup GraalVM
uses: graalvm/setup-graalvm@v1
with:
Expand Down
8 changes: 1 addition & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,18 +87,12 @@ JAR files will be available in each module's `build/libs/` directory.

Most tests require [our mock server](https://github.com/stoplightio/prism) to be running against the OpenAPI spec to work.

The test script will automatically start the mock server for you (if it's not already running) and run the tests against it:
The test script will automatically start the mock server for you and run the tests against it:

```sh
$ ./scripts/test
```

You can also manually start the mock server if you want to run tests repeatedly:

```sh
$ ./scripts/mock
```

Then run the tests:

```sh
Expand Down
2 changes: 2 additions & 0 deletions openai-java-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ dependencies {
testImplementation("org.mockito:mockito-core:5.14.2")
testImplementation("org.mockito:mockito-junit-jupiter:5.14.2")
testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")
testImplementation("org.testcontainers:testcontainers:1.19.8")
testImplementation("org.testcontainers:junit-jupiter:1.19.8")
}

if (project.hasProperty("graalvmAgent")) {
Expand Down
149 changes: 123 additions & 26 deletions openai-java-core/src/test/kotlin/com/openai/TestServerExtension.kt
Original file line number Diff line number Diff line change
@@ -1,39 +1,143 @@
package com.openai

import java.io.File
import java.lang.RuntimeException
import java.net.URL
import java.time.Duration
import org.junit.jupiter.api.extension.BeforeAllCallback
import org.junit.jupiter.api.extension.ConditionEvaluationResult
import org.junit.jupiter.api.extension.ExecutionCondition
import org.junit.jupiter.api.extension.ExtensionContext
import org.testcontainers.containers.GenericContainer
import org.testcontainers.containers.wait.strategy.Wait
import org.testcontainers.utility.DockerImageName
import org.testcontainers.utility.MountableFile

class TestServerExtension : BeforeAllCallback, ExecutionCondition {

override fun beforeAll(context: ExtensionContext?) {
try {
URL(BASE_URL).openConnection().connect()
} catch (e: Exception) {
throw RuntimeException(
"""
The test suite will not run without a mock Prism server running against your OpenAPI spec.
companion object {
private const val INTERNAL_PORT = 4010 // Port inside the container

You can set the environment variable `SKIP_MOCK_TESTS` to `true` to skip running any tests
that require the mock server.
val BASE_URL: String
get() = "http://${prismContainer.host}:${prismContainer.getMappedPort(INTERNAL_PORT)}"

To fix:
const val SKIP_TESTS_ENV: String = "SKIP_MOCK_TESTS"
private const val NODEJS_IMAGE = "node:22"
private const val PRISM_CLI_VERSION = "5.8.5"
private const val API_SPEC_PATH = "/app/openapi.yml" // Path inside the container

1. Install Prism (requires Node 16+):
// Track if the container has been started
private var containerStarted = false

private fun getOpenApiSpecPath(): String {
// First check environment variable
val envPath = System.getenv("OPENAPI_SPEC_PATH")
if (envPath != null) {
return envPath
}

// Try to read from .stats.yml file
try {
val statsFile = File("../.stats.yml")
if (statsFile.exists()) {
val content = statsFile.readText()
val urlLine = content.lines().find { it.startsWith("openapi_spec_url:") }
if (urlLine != null) {
val url = urlLine.substringAfter("openapi_spec_url:").trim()
if (url.isNotEmpty()) {
return url
}
}
}
} catch (e: Exception) {
println(
"Could not read .stats.yml fails, fall back to default. Error is: ${e.message}"
)
}
return "/tmp/openapi.yml"
}

private val prismContainer: GenericContainer<*> by lazy {
val apiSpecPath = getOpenApiSpecPath()
println("Using OpenAPI spec path: $apiSpecPath")
val isUrl = apiSpecPath.startsWith("http://") || apiSpecPath.startsWith("https://")

// Create container with or without copying the file based on whether apiSpecPath is a
// URL
val container =
GenericContainer(DockerImageName.parse(NODEJS_IMAGE))
.withExposedPorts(INTERNAL_PORT)
.withCommand(
"npm",
"exec",
"--package=@stainless-api/prism-cli@$PRISM_CLI_VERSION",
"--",
"prism",
"mock",
apiSpecPath,
"--host",
"0.0.0.0",
"--port",
INTERNAL_PORT.toString(),
)
.withReuse(true)

// Only copy the file to the container if apiSpecPath is a local file
if (!isUrl) {
try {
val file = File(apiSpecPath)
if (file.exists()) {
container.withCopyToContainer(
MountableFile.forHostPath(apiSpecPath),
API_SPEC_PATH,
)
} else {
println("OpenAPI spec file not found at: $apiSpecPath")
throw RuntimeException("OpenAPI spec file not found at: $apiSpecPath")
}
} catch (e: Exception) {
println("Error reading OpenAPI spec file: ${e.message}")
throw RuntimeException("Error reading OpenAPI spec file: $apiSpecPath", e)
}
}

// Add waiting strategy
container.waitingFor(
Wait.forLogMessage(".*Prism is listening.*", 1)
.withStartupTimeout(Duration.ofSeconds(300))
)

With npm:
$ npm install -g @stoplight/prism-cli
// Start the container here once during lazy initialization
container.start()
containerStarted = true
println(
"Prism container started at: ${container.host}:${container.getMappedPort(INTERNAL_PORT)}"
)

With yarn:
$ yarn global add @stoplight/prism-cli
container
}

2. Run the mock server
// Method to ensure container is started, can be called from beforeAll
fun ensureContainerStarted() {
if (!containerStarted) {
// This will trigger lazy initialization and start the container
prismContainer
}
}
}

To run the server, pass in the path of your OpenAPI spec to the prism command:
$ prism mock path/to/your.openapi.yml
override fun beforeAll(context: ExtensionContext?) {
try {
// Use the companion method to ensure container is started only once
ensureContainerStarted()
} catch (e: Exception) {
throw RuntimeException(
"""
Failed to connect to Prism mock server running in TestContainer.

You can set the environment variable `SKIP_MOCK_TESTS` to `true` to skip running any tests
that require the mock server.

You may also need to set `OPENAPI_SPEC_PATH` to the path of your OpenAPI spec file.
"""
.trimIndent(),
e,
Expand All @@ -52,11 +156,4 @@ class TestServerExtension : BeforeAllCallback, ExecutionCondition {
)
}
}

companion object {

val BASE_URL = System.getenv("TEST_API_BASE_URL") ?: "http://localhost:4010"

const val SKIP_TESTS_ENV: String = "SKIP_MOCK_TESTS"
}
}
41 changes: 0 additions & 41 deletions scripts/mock

This file was deleted.

48 changes: 0 additions & 48 deletions scripts/test
Original file line number Diff line number Diff line change
Expand Up @@ -4,53 +4,5 @@ set -e

cd "$(dirname "$0")/.."

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color

function prism_is_running() {
curl --silent "http://localhost:4010" >/dev/null 2>&1
}

kill_server_on_port() {
pids=$(lsof -t -i tcp:"$1" || echo "")
if [ "$pids" != "" ]; then
kill "$pids"
echo "Stopped $pids."
fi
}

function is_overriding_api_base_url() {
[ -n "$TEST_API_BASE_URL" ]
}

if ! is_overriding_api_base_url && ! prism_is_running ; then
# When we exit this script, make sure to kill the background mock server process
trap 'kill_server_on_port 4010' EXIT

# Start the dev server
./scripts/mock --daemon
fi

if is_overriding_api_base_url ; then
echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}"
echo
elif ! prism_is_running ; then
echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server"
echo -e "running against your OpenAPI spec."
echo
echo -e "To run the server, pass in the path or url of your OpenAPI"
echo -e "spec to the prism command:"
echo
echo -e " \$ ${YELLOW}npm exec --package=@stoplight/prism-cli@~5.3.2 -- prism mock path/to/your.openapi.yml${NC}"
echo

exit 1
else
echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}"
echo
fi

echo "==> Running tests"
./gradlew test
Loading