Run the tunnel inside your JVM
The tunnel runs as a library as well as a process. Start it from a JUnit or TestNG fixture, wait until it can carry traffic, and stop it when the suite ends. No separate process to manage, no shell script in CI.
- Since v5.0
- Java 17+
Dependency
<dependency>
<groupId>com.testingbot</groupId>
<artifactId>TestingBotTunnel</artifactId>
<version>5.0</version>
<scope>test</scope>
</dependency>
com.testingbot:TestingBotTunnel:5.0 is the library artifact: Jetty, Apache HttpClient, JSch and its other dependencies are declared in its POM, so Maven or Gradle resolves them alongside your own and can settle version conflicts. The downloadable testingbot-tunnel.jar (the shaded classifier on Maven Central) is built to run on its own: it bundles those libraries under their original package names, so on your classpath they would duplicate, and possibly clash with, the versions your project already uses.
Lifecycle
Configure an App with the same settings the command line takes, then boot(), awaitReady() and stop().
import com.testingbot.tunnel.App;
import java.time.Duration;
App tunnel = new App();
tunnel.setClientKey(System.getenv("TESTINGBOT_KEY"));
tunnel.setClientSecret(System.getenv("TESTINGBOT_SECRET"));
tunnel.setSeleniumPort(4445);
tunnel.setJettyPort(8087); // local proxy, --localproxy
tunnel.setMetricsPort(8003);
try {
tunnel.boot(); // throws TunnelFailedException instead of exiting the JVM
if (tunnel.awaitReady(Duration.ofMinutes(2))) {
// run the tests
} else {
throw new IllegalStateException("tunnel not ready: " + tunnel.getSetupFailure());
}
} finally {
tunnel.stop();
}
| Method | What it does |
|---|---|
| boot() | Creates the tunnel. Returns once it has been created, which is not the same as it being able to carry traffic: if the tunnel server is still starting, the rest happens in the background. |
| awaitReady(Duration) | Waits for the same state /readyz reports. Returns false as soon as setup gives up, so you do not wait out the timeout for a tunnel that has already failed. |
| getSetupFailure() | Why setup failed, for your error message. |
| isReady() | The current readiness, without waiting. |
| stop() | Shuts the tunnel down and releases everything it held. |
Most command-line options have a matching setter, for example setTunnelIdentifier, setShared, setBindAddress and setProxy.
One tunnel per test class
import com.testingbot.tunnel.App;
import java.time.Duration;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
class CheckoutIT {
static App tunnel;
@BeforeAll
static void startTunnel() throws Exception {
tunnel = new App();
tunnel.setClientKey(System.getenv("TESTINGBOT_KEY"));
tunnel.setClientSecret(System.getenv("TESTINGBOT_SECRET"));
tunnel.setTunnelIdentifier("checkout-it");
tunnel.boot();
if (!tunnel.awaitReady(Duration.ofMinutes(2))) {
throw new IllegalStateException("tunnel not ready: " + tunnel.getSetupFailure());
}
}
@AfterAll
static void stopTunnel() {
if (tunnel != null) tunnel.stop();
}
// tests use http://localhost:4445/wd/hub with tunnel-identifier "checkout-it"
}
What you can rely on
-
Nothing calls
System.exit. Every failure while booting throwsTunnelFailedException. A failure in the background, where there is nobody to throw to, releases everything and leaves a stopped tunnel whoseisReady()and/readyzsay so. -
stop()gives everything back. The Selenium relay, local proxy and metrics port are unbound, the SSH session and its timers are cancelled, shutdown hooks are unregistered, the ready file is removed, and the JVM's defaultAuthenticatoris put back the way it was found. Starting a tunnel per job does not leak. - Readiness is per tunnel. Two tunnels in one JVM each answer for themselves, on their own metrics port.
-
Your logging is left alone. Console handlers, log levels and
--log-formatare set up by the command-line entry point, not byboot().
Shared within a JVM
Three things are process-wide, because what they are built on is.
- The Prometheus registry behind
/metricsand the request and byte counters on/, which add up across tunnels in the same JVM. -
Authenticator.setDefault, which the JDK uses for proxy and SOCKS credentials. The tunnel's authenticator answers only for the proxy it was given and delegates everything else to whatever was installed before it. - Uptime, measured from the first tunnel to boot.
Not on the JVM? The NodeJS launcher and the GitHub Action manage the tunnel process for you.