Testing on embedded devices with embedded-test 📋
- Timo Lang
- 5. Dez. 2024
- 3 Min. Lesezeit
Aktualisiert: vor 2 Tagen
Testing embedded systems can be challenging. While Rust’s built-in test framework is fantastic for host environments, its reliance on the standard library (std) makes it unsuitable for bare-metal platforms. This often forces developers to skip tests entirely or test only the host-compatible portions of their code.
But what if you could bring Rust's ergonomic testing experience to embedded systems?
Meet embedded-test, a test harness and runner for embedded devices that allows you to run your integration tests on the target hardware with the same simplicity as on your host machine.
The Problem with Rust’s Built-in Test Framework 🚧
If you’re new to Rust’s test framework, here’s an example:
/// Computes the factorial of a number.
pub fn factorial(n: u32) -> u32 {
match n {
0 | 1 => 1,
_ => n * factorial(n - 1),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_factorial() {
assert_eq!(factorial(5), 120); // Typical case
}
#[test]
#[should_panic]
fn test_factorial_overflow() {
factorial(35); // This will overflow for `u32`
}
}
Running tests is as simple as invoking cargo test:
$ cargo test
running 2 tests
test tests::test_factorial ... ok
test tests::test_factorial_overflow - should panic ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; finished in 0.00s
However, compiling this for a bare-metal platform (e.g., STM32) typically results in an error like error[E0463]: can't find crate for std
Even in std contexts, you’d still need a custom runner to execute tests. As a result, many developers either skip testing entirely or limit it to host-compatible code. While testing on the host is valuable, we can and should go further.
Introducing embedded-test 🚀
embedded-test is a test harness and runner designed for embedded systems. It brings many features you’re used to in host testing to your embedded projects:
Run tests directly on the target hardware.
Use Rust’s familiar #[test] attribute and annotations like #[should_panic].
Get detailed test results, integrated into your IDE or CI/CD pipelines.
Getting Started with embedded-test
Here’s how to set up and use embedded-test:
Write Your Tests
Move your tests into the tests/ directory as Rust integration tests (instead of inline unit tests in src).
Add embedded_test::tests
Annotate your test module with #[embedded_test::tests]:
#[cfg(test)]
#[embedded_test::tests]
mod tests {
// Test cases
}
Update Your Cargo.toml
Add the embedded-test dependency and disable the standard harness for the test file:
[dev-dependencies]
embedded-test = "0.6.0"
[[test]]
name = "example_test"
harness = false
Include the Linker Script
Add this to your build.rs:
fn main() {
println!("cargo:rustc-link-arg-tests=-Tembedded-test.x");
}
Install Probe-RS
Install probe-rs with cargo binstall probe-rs-tools.
Configure Your Target Runner
Add the following to .cargo/config.toml for your target (e.g., ESP32-C6):
[target.riscv32imac-unknown-none-elf]
runner = "probe-rs run --chip esp32c6"
Running Tests
With the setup complete, simply run cargo test:
$ cargo test
Erasing ✔ 100% [####################] 192.00 KiB @ 556.44 KiB/s (took 0s)
Programming ✔ 100% [####################] 48.99 KiB @ 59.70 KiB/s (took 1s) Finished in 0.82s
running 2 tests
test tests::test_factorial ... ok
test tests::test_factorial_overflow ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.47s
probe-rs flashes all your tests to the device and executes them, resetting the chip after each test.
Features of embedded-test ⭐
embedded-test is fully compatible with Rust’s libtest, offering additional features:
Async Test Functions
Write async tests, just like with tokio::test. Needs feature embassy.
#[test]
async fn async_test_example() {
// Async test logic
}
Global Setup and State Sharing
Run a global initialization function before each test, passing shared state:
#[init]
fn init() -> Peripherals {
Peripherals::take().unwrap()
}
#[test]
fn takes_state(state: Peripherals) {
// Test logic using Peripheral
}
Advanced Test Annotations
Use annotations like #[should_panic], #[ignore], or #[timeout(seconds)].
CI/CD Integration and IDE Integration
Get JSON or JUnit output (via cargo2junit) to validate tests in pipelines or to view test results directly in your IDE.

What’s Next?
Version 0.6.0 is out with updated examples for STM32 and ESP32. Future plans include:
Reading test lists directly from the ELF file to improve unit test support.
Enabling host-side code execution for test setup and teardown, such as controlling power analyzers.
Try It Out! 🧑💻
Curious about testing your embedded code with Rust? Check out:
I’d love your feedback—feel free to open an issue or start a discussion!
Comments