Unit tests that make real network requests are:
- Slow - Network I/O is orders of magnitude slower than in-memory operations
- Flaky - Tests fail randomly due to network issues, timeouts, or service downtime
- Dangerous - Tests can accidentally modify production data or trigger side effects
- Hidden - Hard to spot network dependencies in code review
- Environment-dependent - Pass locally but fail in CI (or vice versa)
The solution: Automatically fail any test that attempts a network request. Force yourself to use mocks, fakes, or test doubles instead.
@Test
fun `calculates user stats`() {
val stats = userService.calculateStats(userId = 123)
assertEquals(42, stats.totalPurchases)
}What's wrong? This test looks innocent, but if userService internally makes an HTTP request to fetch user data, you've got a hidden network dependency. The test will:
- Take 500ms+ instead of <1ms
- Fail when the API is down
- Potentially hit production servers
@Test
@BlockNetworkRequests // ← Fails immediately if network is accessed
fun `calculates user stats`() {
val fakeService = FakeUserService(
users = listOf(User(id = 123, purchases = 42))
)
val stats = fakeService.calculateStats(userId = 123)
assertEquals(42, stats.totalPurchases) // Fast, reliable, isolated ✅
}plugins {
id("io.github.garry-jeromson.junit-airgap") version "0.1.0-beta.1"
}That's it for setup! The plugin automatically configures everything.
JUnit 5:
@Test
@BlockNetworkRequests
fun `test with no network access`() {
// This will throw NetworkRequestAttemptedException
Socket("example.com", 80)
}JUnit 4:
@Test
@BlockNetworkRequests
fun testWithNoNetworkAccess() {
// This will throw NetworkRequestAttemptedException
Socket("example.com", 80)
}./gradlew testAny test annotated with @BlockNetworkRequests will now fail fast if it attempts network I/O:
io.github.garryjeromson.junit.airgap.NetworkRequestAttemptedException:
Network request blocked: example.com:80
at MyTest.testWithNoNetworkAccess(MyTest.kt:15)
That's it! 🎉
Uses a JVMTI agent (JVM Tool Interface) to intercept network calls at the native level:
- C++ agent intercepts sockets and DNS - Catches all network operations before they reach the network stack
- Auto-loaded at JVM startup - Plugin automatically extracts and loads the native agent
- Checks against configuration - Evaluates allowed/blocked hosts with wildcard support
- Fails fast - Throws
NetworkRequestAttemptedExceptionimmediately - Works everywhere - Same implementation for JVM and Android (via Robolectric)
Key benefits:
- ✅ Works on any Java version (JVMTI is not version-dependent)
- ✅ Requires Java 21+ for build (Kotlin Gradle Plugin requirement)
- ✅ Intercepts ALL HTTP clients (works at socket level)
- ✅ Catches both hostname and IP address connections
- ✅ Includes DNS interception for complete coverage
- ✅ Zero-configuration with Gradle plugin
| Category | Status | Details |
|---|---|---|
| Java | Java 21+ | Single JVMTI agent works across all 21+ versions |
| JUnit | 4.13.2 & 5.11.3 | Both frameworks fully supported |
| Platform | JVM (macOS ARM64) | Native agent for macOS ARM64 |
| Platform | JVM (Linux x86-64) | Native agent for Linux x86-64 |
| Platform | Android (Robolectric) | Full support via Robolectric unit tests |
| HTTP Clients | All major clients | OkHttp, Retrofit, Ktor, Apache, Spring, etc. |
- Linux ARM64
- iOS/Kotlin Native: Platform limitations prevent comprehensive network interception. See
IOS_SUPPORT_INVESTIGATION.mdfor technical details. - Windows: Not planned for support.
- macOS Intel (x86-64): Not planned for support.
For complete compatibility information including:
- Specific HTTP client versions tested
- Platform architecture details
- Exception handling by client
- Known limitations
See the Compatibility Matrix →
Step-by-step instructions for your project type:
- JVM + JUnit 5 - Pure JVM projects with JUnit 5
- JVM + JUnit 4 - Pure JVM projects with JUnit 4
- Android + Robolectric - Android unit tests
- Kotlin Multiplatform + JUnit 5 - KMP with JUnit 5
- Kotlin Multiplatform + JUnit 4 - KMP with JUnit 4
- Gradle Plugin Reference - Complete plugin configuration
Client-specific examples and exception handling:
- OkHttp - Most popular Android/JVM HTTP client
- Retrofit - Type-safe HTTP client
- Ktor - Kotlin Multiplatform HTTP client
- Advanced Configuration - All configuration options
- Compatibility Matrix - Complete compatibility info
junitAirgap {
applyToAllTests = true // Block by default
}@Test
fun test1() {
// Network blocked automatically
}
@Test
@AllowNetworkRequests // Opt-out when needed
fun test2() {
// Network allowed
}Perfect for testing with local servers or staging environments:
@Test
@BlockNetworkRequests
@AllowRequestsToHosts(["localhost", "127.0.0.1", "*.staging.mycompany.com"])
fun testWithStagingAPI() {
// ✅ localhost - allowed
// ✅ api.staging.mycompany.com - allowed
// ❌ api.production.mycompany.com - blocked
// ❌ external-api.com - blocked
}junitAirgap {
enabled = true
applyToAllTests = false
allowedHosts = listOf("localhost", "*.test.local")
blockedHosts = listOf("*.tracking.com")
debug = false
}More examples: Advanced Configuration Guide →
All tested with comprehensive integration tests:
Core:
- ✅ Raw sockets (
Socket,ServerSocket) - ✅ Java HTTP (
HttpURLConnection,HttpClient)
Popular Libraries:
- ✅ OkHttp 4.12.0
- ✅ Retrofit 2.11.0
- ✅ Ktor 2.3.7 (CIO, OkHttp, Java engines)
- ✅ Apache HttpClient5 5.3.1
- ✅ Reactor Netty HTTP 1.1.22
- ✅ AsyncHttpClient 3.0.0
- ✅ Spring WebClient 6.2.0
- ✅ OpenFeign 13.5
- ✅ Fuel 2.3.1
- ✅ Android Volley 1.2.1
Exception handling varies by client - some throw NetworkRequestAttemptedException directly, others wrap it in IOException. See HTTP Client Guides for details.
Zero configuration - plugin handles everything automatically:
plugins {
id("io.github.garry-jeromson.junit-airgap") version "0.1.0-beta.1"
}Requires manual configuration (see Setup Guides for details):
dependencies {
testImplementation("io.github.garryjeromson:junit-airgap:0.1.0-beta.1")
}The JVMTI agent loads once at JVM startup and has minimal overhead:
- Agent loading: ONE TIME at startup (~5-10ms)
- Per-test overhead: ~100-500 nanoseconds
- Real-world impact: <10% for tests doing meaningful work
From benchmark suite (100 iterations, Java 21):
| Test Type | Overhead | Notes |
|---|---|---|
| Empty Test | +458 ns (+183%) | High % but negligible absolute time |
| Array Sorting (4.2ms) | +270 μs (+6.4%) | Realistic test - low overhead |
Key insight: Small constant overhead appears as high percentage for nanosecond operations, but is negligible for real tests.
Run benchmarks: make benchmark
Learn more about JVMTI performance →
# Run all tests
make test
# Run specific test suites
make test-jvm # JVM only
make test-android # Android only
make test-integration # Integration tests
make test-plugin-integration # Plugin integration tests
# Test on Linux locally (requires Docker)
make docker-build-linux # Build Linux Docker image (one-time)
make docker-test-linux # Run tests in Linux containerDocker Multi-Platform Testing: Test Linux builds locally before pushing to CI for faster feedback loops. See Docker Local Testing Guide →
Checklist:
- ✅ Is
@BlockNetworkRequestsannotation present? - ✅ For JUnit 5: Is
@ExtendWith(AirgapExtension::class)on class? - ✅ Is JVMTI agent loaded? (check with
-Djunit.airgap.debug=true)
Issue Fixed in v0.1.0-beta.2+
If you're using an older version and encounter "platform encoding not initialized" errors when running tests via IntelliJ:
Workaround 1: Configure IntelliJ to use Gradle
Preferences → Build, Execution, Deployment → Build Tools → Gradle
Set "Run tests using" to "Gradle" instead of "IntelliJ IDEA"
Workaround 2: Run tests via Gradle
./gradlew test --tests "YourTestClass"Solution: Upgrade to v0.1.0-beta.2 or later for full IDE support.
WARNING: JVMTI agent not found at: /path/to/agent.dylib
Solution:
./gradlew clean build # Rebuild to extract agentVerify plugin is enabled:
junitAirgap {
enabled = true
}java.lang.UnsatisfiedLinkError: no junit-airgap-agent in java.library.path
Current support: macOS ARM64, Linux x86-64
Coming soon: Linux ARM64
See Platform Compatibility for details.
See detailed information about what's being blocked:
./gradlew test -Djunit.airgap.debug=trueOr in build.gradle.kts:
tasks.test {
systemProperty("junit.airgap.debug", "true")
}- 📖 Setup Guides - Step-by-step instructions
- 📊 Compatibility Matrix - Verify your setup
- 🐛 GitHub Issues - Report bugs or ask questions
Complete working examples in plugin-integration-tests/:
- test-contracts - Shared test assertions (used by all projects)
- jvm-junit4 - JVM with JUnit 4
- jvm-junit5 - JVM with JUnit 5
- android-robolectric - Android with Robolectric
- kmp-junit4 - Kotlin Multiplatform with JUnit 4
- kmp-junit5 - Kotlin Multiplatform with JUnit 5
- kmp-kotlintest - KMP with kotlin.test + JUnit 4 runtime
- kmp-kotlintest-junit5 - KMP with kotlin.test + JUnit 5 runtime
Contributions welcome! See CONTRIBUTING.md for guidelines.
MIT License - See LICENSE for details
Built with comprehensive test coverage across all platforms, frameworks, and HTTP clients.
This project was inspired by similar network blocking tools in other ecosystems:
- pytest-socket - Socket blocking for Python's pytest framework. The original inspiration for bringing this pattern to JVM/Android.
- Sniffy - SQL and network monitoring for Java applications. Demonstrated that comprehensive network interception is achievable on the JVM.
Made with ❤️ for better unit tests
