A minimalist Android test framework for Rust that enables running Rust tests on Android devices through dynamic loading.
Named after the vampire crab (Geosesarma dennerle), a small purple crustacean native to Java. The name is a double pun: the crab lives in Java (the island), and this framework runs tests in Java (the Android platform). Like its namesake, Vampire dynamically loads into a host environment to do its work.
-
Install dependencies:
# Ensure you have Android SDK and NDK export ANDROID_SDK_ROOT=/path/to/android/sdk # Or on macOS, SDK is auto-detected from ~/Library/Android/sdk # Install cargo-ndk cargo install cargo-ndk
-
Run tests:
# Run tests (builds + deploys + runs automatically) cargo run --bin vampire -- test # Show detailed test output including stdout/stderr cargo run --bin vampire -- test --nocapture # Force rebuild of APK even if already installed cargo run --bin vampire -- test --force
The
vampireCLI automatically:- β Builds test library for Android (arm64-v8a)
- β Compiles and builds host APK (only when needed)
- β Installs APK to device (only if not already installed)
- β Deploys native library to app's private storage
- β Runs tests and displays results
Specify Android permissions your tests need in Cargo.toml:
[package.metadata.vampire]
permissions = [
"android.permission.INTERNET",
"android.permission.ACCESS_NETWORK_STATE",
"android.permission.WRITE_EXTERNAL_STORAGE"
]These permissions are automatically added to the generated AndroidManifest.xml when building the host APK.
If your tests require Android libraries (JARs/AARs), specify them using Maven coordinates in a Cargo-style format:
[package.metadata.vampire.dependencies]
"org.chromium.net:cronet-api" = "141.7340.3"
"androidx.annotation:annotation" = "1.7.0"You can also use the object form for future extensibility:
[package.metadata.vampire.dependencies]
"org.chromium.net:cronet-api" = { version = "141.7340.3" }Vampire will automatically:
- Download the artifacts and their transitive dependencies from Maven Central and Google Maven
- Resolve version conflicts using Maven's nearest-wins strategy
- Extract classes from AAR files
- Include them in the DEX compilation for the host APK
- Cache downloads in
target/vampire/maven-cache/for faster builds
This allows your Rust tests to interact with Java classes via JNI.
If you have Java sources that need to be compiled for Android (e.g., JNI callback classes), create a build.rs file in your package root:
fn main() {
vampire_build::configure();
}Add vampire-build to your build dependencies in Cargo.toml:
[build-dependencies]
vampire-build = { path = "../vampire/vampire-build" } # or version from crates.ioBy default, vampire-build looks for Java sources in the java/ directory of your package. Place your .java files there:
my-package/
βββ Cargo.toml
βββ build.rs
βββ java/
β βββ com/
β βββ example/
β βββ MyCallback.java
βββ src/
βββ lib.rs
For advanced use cases, you can customize the builder:
fn main() {
vampire_build::Builder::new()
.java_dir("src/main/java") // Custom Java source directory
.target_sdk(33) // Target SDK version (default: 30)
.java_source("path/to/specific/File.java") // Add specific file
.configure();
}vampire-build automatically:
- Enables
cfg(vampire)for conditional compilation - Detects Android target builds
- Finds and compiles all
.javafiles in your Java directory - Uses Java 8 compatibility for Android
- Converts
.classfiles to DEX format (required for Android) - Outputs
classes.dextoOUT_DIR - Sets up proper
cargo:rerun-if-changedtriggers
Note: DEX generation requires the Android SDK with build-tools installed. Set ANDROID_SDK_ROOT or ANDROID_HOME environment variable pointing to your SDK location.
use vampire;
// IMPORTANT: Re-export JNI_OnLoad so Java can initialize the runtime
pub use vampire::JNI_OnLoad;
#[vampire::test]
fn sync_test() {
assert_eq!(2 + 2, 4);
}
#[vampire::test]
async fn async_test() {
tokio::time::sleep(Duration::from_millis(100)).await;
// Your async test code
}Use should_panic for tests that are expected to fail:
#[vampire::test(should_panic)]
fn test_expected_failure() {
panic!("This failure is expected and will count as a pass");
}Access Android system properties and device information:
use vampire::android;
#[vampire::test]
fn test_android_system() {
// Get Android version
let version = android::get_android_version()
.expect("Should get Android version");
// Access app's private directory
let files_dir = android::get_files_dir()
.expect("Should get files directory");
// Query device information
let model = android::get_device_model();
let is_emulator = android::is_emulator();
// Check system properties
let prop = android::get_system_property("ro.build.version.sdk");
}The #[vampire::test] macro works on both Android and native platforms:
#[vampire::test]
fn test_works_everywhere() {
assert_eq!(2 + 2, 4);
}- On Android: Runs through the instrumentation framework
- On native: Expands to
#[test](or#[tokio::test]for async)
Run native tests with cargo test, Android tests with cargo run --bin vampire -- test.
Since Android requires all tests in a single cdylib, you need to explicitly include tests/ files in your lib.rs:
// In src/lib.rs
#[cfg(vampire)]
#[path = "../tests"]
mod integration_tests {
#[path = "my_test.rs"]
mod my_test;
#[path = "another_test.rs"]
mod another_test;
}The vampire CLI automatically sets --cfg vampire when building, so these modules are only included when building for Android tests.
- Host APK: Minimal instrumentation container that loads test libraries
- Test Library: Your Rust tests compiled to
.sowith automatic JNI registration - vampire CLI: Coordinates building, deploying, and running tests
vampire-cli: Main CLI tool (cargo run --bin vampire)vampire-macro: Proc macro providing#[vampire::test]vampire: Runtime library with JNI utilities and Android system accessvampire-example: Example test suite demonstrating features