# How to Write Integration Tests for iroh Applications with Connection Monitoring

> Learn to write integration tests for iroh applications using Rust tokio tests and custom EndpointHooks to monitor QUIC handshakes and connection stats. Improve your iroh app testing today.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: testing
- Published: 2026-06-22

---

**Integration tests for iroh applications use standard Rust `#[tokio::test]` functions that instantiate `Endpoint` objects with custom `EndpointHooks` implementations to capture QUIC handshake metadata and log final connection statistics when connections close.**

Writing robust integration tests for the n0-computer/iroh networking stack requires more than simple request-response validation. By implementing the `EndpointHooks` trait, you can monitor connection lifecycles within your tests, capturing ALPN identifiers, remote endpoint IDs, and final transfer statistics. This pattern enables deterministic verification of connection health while providing deep visibility into QUIC protocol behavior.

## The Connection Monitoring Architecture

The connection monitoring pattern in iroh relies on the `EndpointHooks` trait defined in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs). This trait provides an interception point immediately after the QUIC handshake completes, allowing your test infrastructure to record connection metadata and spawn background tasks that observe connection lifecycle events.

### Capturing Handshake Data with EndpointHooks

When you implement `EndpointHooks`, the `after_handshake` method receives a `Connection` reference right after the cryptographic handshake succeeds. This is the ideal location to capture immutable connection properties like the ALPN identifier and remote endpoint ID, as well as to obtain a `WeakConnectionHandle` for later observation.

According to the reference implementation in `iroh/examples/monitor-connections.rs#L82-L100`, the hook should:
1. Extract the ALPN and remote endpoint ID from the connection
2. Create a `WeakConnectionHandle` to avoid blocking the connection's drop
3. Send this data through an async channel to a dedicated watcher task

### Monitoring Connection Closure

The background watcher task awaits the `WeakConnectionHandle::closed()` future, which resolves to a `Closed` struct containing final statistics. As shown in `iroh/examples/monitor-connections.rs#L130-L144`, this struct provides access to `udp_rx.bytes`, `udp_tx.bytes`, and path RTT measurements, enabling your tests to verify that data actually flowed through the connection.

## Implementing a Custom Connection Monitor

The monitor implementation requires three components: a struct holding the channel sender, a message type for handshake data, and the `EndpointHooks` trait implementation.

```rust
// Pattern based on iroh/examples/monitor-connections.rs#L27-59
use iroh::{
    endpoint::{EndpointHooks, Connection, WeakConnectionHandle},
    EndpointId,
};
use tokio::sync::mpsc::{UnboundedSender, UnboundedReceiver};
use tracing::info;

#[derive(Clone, Debug)]
pub struct Monitor {
    tx: UnboundedSender<MonitoredConnection>,
    _task: std::sync::Arc<tokio::task::JoinHandle<()>>,
}

#[derive(Debug)]
pub struct MonitoredConnection {
    alpn: Vec<u8>,
    remote_id: EndpointId,
    handle: WeakConnectionHandle,
}

impl EndpointHooks for Monitor {
    async fn after_handshake(&self, conn: &Connection) -> iroh::endpoint::AfterHandshakeOutcome {
        let info = MonitoredConnection {
            alpn: conn.alpn().to_vec(),
            remote_id: conn.remote_id(),
            handle: conn.weak_handle(),
        };
        // Ignore send errors – they only happen when the watcher has shut down
        let _ = self.tx.send(info);
        iroh::endpoint::AfterHandshakeOutcome::Accept
    }
}

impl Monitor {
    pub fn new() -> Self {
        let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
        let task = tokio::spawn(Self::run(rx));
        Self {
            tx,
            _task: std::sync::Arc::new(task),
        }
    }

    async fn run(mut rx: UnboundedReceiver<MonitoredConnection>) {
        let mut tasks = tokio::task::JoinSet::new();
        
        while let Some(MonitoredConnection { alpn, remote_id, handle }) = rx.recv().await {
            let alpn_str = String::from_utf8_lossy(&alpn).to_string();
            let remote_short = remote_id.fmt_short();
            
            info!(remote = %remote_short, alpn = %alpn_str, "new connection");

            tasks.spawn(async move {
                match handle.closed().await {
                    Some(closed) => {
                        let rtt = handle.upgrade()
                            .and_then(|c| c.paths().iter().map(|p| p.rtt()).min());
                        
                        info!(
                            remote = %remote_short,
                            alpn = %alpn_str,
                            ?rtt,
                            udp_rx = closed.stats.udp_rx.bytes,
                            udp_tx = closed.stats.udp_tx.bytes,
                            "connection closed"
                        );
                    }
                    None => {
                        info!(remote = %remote_short, alpn = %alpn_str, 
                              "connection closed before stats captured");
                    }
                }
            });
        }
        
        // Drain remaining tasks
        while let Some(res) = tasks.join_next().await {
            let _ = res;
        }
    }
}

```

## Writing the Integration Test

Integration tests follow the pattern established in [`iroh/tests/integration.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/integration.rs). You instantiate endpoints using the builder API, attach your monitor via `.hooks()`, drive traffic between nodes, and optionally assert on the collected metrics.

### Setting Up Monitored Endpoints

Build your server endpoint with the monitor attached, as demonstrated in `iroh/examples/monitor-connections.rs#L27-L31`. The client endpoint may also include a monitor if you need visibility into both sides of the connection.

```rust
// Based on iroh/tests/integration.rs#L35-80 and integration.rs#L22-30
#[tokio::test]
async fn integration_with_monitor() -> iroh::Result<()> {
    // Initialize tracing to see monitor output in test logs
    tracing_subscriber::fmt().with_env_filter("info").init();

    // Build server with monitoring hooks
    let monitor = Monitor::new();
    let server = iroh::Endpoint::builder(iroh::endpoint::presets::Minimal)
        .hooks(monitor.clone())
        .alpns(vec![b"test".to_vec()])
        .bind()
        .await?;

    // Build client (monitor optional here)
    let client = iroh::Endpoint::builder(iroh::endpoint::presets::Minimal)
        .bind()
        .await?;

    // Drive traffic: connect, open bidirectional stream, send data
    let conn = client.connect(server.addr(), b"test").await?;
    let (mut send, mut recv) = conn.open_bi().await?;
    
    send.write_all(b"ping").await?;
    send.finish().await?;
    
    let resp = recv.read_to_end(1024).await?;
    assert_eq!(resp, b"ping");

    // Graceful shutdown
    client.close().await;
    server.close().await;
    
    // Allow watcher task to process close events before dropping monitor
    tokio::time::sleep(std::time::Duration::from_millis(200)).await;
    drop(monitor);

    Ok(())
}

```

### Validating Connection Statistics

For deterministic verification, extend the `Monitor` implementation to store `Closed` statistics in a `Vec` or use a `tokio::sync::watch` channel to make them available to the test thread. The [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs) module contains additional network monitoring utilities compatible with this pattern if you require deeper packet-level inspection.

## Running the Tests

Execute your integration tests using the standard cargo test runner. The monitor's tracing output appears in the test logs, providing visibility into connection lifecycles.

```bash

# Native execution

cargo test integration_with_monitor

# WASM target (requires wasm-bindgen-test)

cargo test --target wasm32-unknown-unknown --test integration_with_monitor

```

When the test runs, you will observe log output from the monitor showing the connection opening and closing with statistics:

```

INFO monitor_watcher: new connection remote=abcd… alpn="test"
INFO monitor_watcher: connection closed remote=abcd… alpn="test" udp_rx=4 udp_tx=4

```

## Summary

- **Implement `EndpointHooks`** to intercept connections immediately after the QUIC handshake in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs)
- **Capture `WeakConnectionHandle`** during `after_handshake` to avoid blocking connection drop while enabling async monitoring
- **Spawn background tasks** that await `handle.closed()` to receive final `Closed` statistics including byte counts and RTT
- **Attach monitors via `.hooks()`** on the `Endpoint` builder when constructing test fixtures
- **Reference [`iroh/examples/monitor-connections.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/monitor-connections.rs)** for the canonical implementation and [`iroh/tests/integration.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/integration.rs) for test structure patterns

## Frequently Asked Questions

### Can I monitor connections in WASM integration tests?

Yes. The same `EndpointHooks` implementation works for WASM targets when using `#[wasm_bindgen_test]` instead of `#[tokio::test]`. The [`iroh/tests/integration.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/integration.rs) file demonstrates this pattern, and the `WeakConnectionHandle` closure tracking operates identically across native and WASM builds.

### How do I assert on connection statistics rather than just logging them?

Modify your `Monitor` struct to include a `tokio::sync::mpsc::Sender` or `Arc<Mutex<Vec<Closed>>>` that the background task populates when `handle.closed()` resolves. In your test, await a short duration after closing endpoints, then query the monitor's stored statistics to assert specific byte counts or RTT thresholds.

### What is the difference between `WeakConnectionHandle` and holding the `Connection` directly?

`WeakConnectionHandle` does not prevent the connection from closing, whereas holding the `Connection` struct would keep the QUIC session alive. The weak handle allows you to monitor closure statistics without interfering with the natural connection lifecycle, which is critical for testing realistic shutdown scenarios.

### Does iroh provide built-in network monitoring beyond EndpointHooks?

Yes. The [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs) module (lines 448-1870) contains `netmon::Monitor` and related utilities for lower-level network interface monitoring. You can combine these socket-level monitors with `EndpointHooks` to capture both connection-level metrics and interface-level packet flows in the same test.