---
title: 'Embed TestingBot Tunnel in Java: Start and Stop the Tunnel from Your Tests'
description: 'Run TestingBot Tunnel as a Java library: boot(), awaitReady() and stop()
  from JUnit or TestNG. Maven coordinates, lifecycle guarantees and what is shared
  between tunnels in one JVM.'
source_url:
  html: https://testingbot.com/support/tunnel/embedding
  md: https://testingbot.com/support/tunnel/embedding.md
---

# 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

```xml
<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()`.

```java
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

```java
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](https://testingbot.com/support/tunnel/installation#node) and the [GitHub Action](https://testingbot.com/support/integrations/ci-cd/github-actions) manage the tunnel process for you.

## Looking for more help?

Have questions or need more information? Reach out via email or Slack.

[Email us](https://testingbot.com/contact/new) [Join our Slack](https://join.slack.com/t/testingb0t/shared_invite/zt-3bcw9xch-jk19~6XPs_xBrsAgAedkCw)
