Skip to main content

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>
Use the regular artifact, not the shaded JAR

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 throws TunnelFailedException. A failure in the background, where there is nobody to throw to, releases everything and leaves a stopped tunnel whose isReady() and /readyz say 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 default Authenticator is 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-format are set up by the command-line entry point, not by boot().

Shared within a JVM

Three things are process-wide, because what they are built on is.

  • The Prometheus registry behind /metrics and 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.

Was this page helpful?
Last updated