embedded-hal-mock

repository·main·Indexed 40 days ago

https://github.com/rust-embedded/embedded-hal-mock

A collection of mock implementations for the embedded-hal traits, version 0.11.1. It enables driver testing in CI and environments without physical hardware by providing mock types for I2C, SPI, ADC, digital I/O, PWM, and delays. The crate supports both embedded-hal version 0.x (via the eh0 feature) and 1.x (via the eh1 feature), as well as embedded-hal-async.

Tokens
13K
Snippets
31
Records
46
Agent score
47%

What's inside embedded-hal-mock

  1. What is embedded-hal-mock?

    main
    The embedded-hal-mock crate provides a collection of types that implement embedded-hal traits. Instead of interacting with real hardware, these implementations use mocks or no-op behaviors. This allows developers to test drivers in CI environments or local development setups where physical hardware is unavailable.
  2. Configure embedded-hal version support via Cargo features

    main

    The crate supports both embedded-hal version 0.x and 1.x. You control which version is available using Cargo features:

    • To use version 0.x: Enable the eh0 feature. This provides the eh0 module.
    • To use version 1.x: Enable the eh1 feature. This provides the eh1 module.

    Note: By default, the crate is configured to support version 1.x (via eh1) and includes the embedded-time feature for eh0::timer support.

  3. Use a development version of embedded-hal

    main

    If you need to test against the current development version of embedded-hal (or any other version that is API compatible), you can use a [patch.crates-io] section in your Cargo.toml to override the dependency.

    [patch.crates-io]
    eh1 = { git = "https://github.com/rust-embedded/embedded-hal" }
  4. Mock digital I/O for embedded-hal 0.x

    main

    The embedded_hal_mock::eh0::digital module provides mock implementations for InputPin, OutputPin, ToggleableOutputPin, and PwmPin traits from embedded-hal version 0.2.x.

    To use these mocks, you define a sequence of Transaction objects representing the expected interactions with the hardware. You then pass these expectations to Mock::new(). During execution, the mock verifies that each call matches the next expected transaction in the sequence. If a call does not match or if there are unexpected calls, the mock will panic or return an error.

    use embedded_hal::digital::v2::{InputPin, OutputPin, ToggleableOutputPin};
    use embedded_hal_mock::eh0::digital::{Mock as PinMock, State as PinState, Transaction as PinTransaction, MockError};
    use std::io::ErrorKind;
    
    let err = MockError::Io(ErrorKind::NotConnected);
    
    // Configure expectations
    let expectations = [
        PinTransaction::get(PinState::High),
        PinTransaction::get(PinState::High),
        PinTransaction::set(PinState::Low),
        PinTransaction::set(PinState::High).with_error(err.clone()),
        PinTransaction::toggle(),
    ];
    
    // Create pin
    let mut pin = PinMock::new(&expectations);
    
    // Run and test
    assert_eq!(pin.is_high().unwrap(), true);
    assert_eq!(pin.is_low().unwrap(), false);
    
    pin.set_low().unwrap();
    pin.set_high().expect_err("expected error return");
    
    pin.toggle().unwrap();
    
    pin.done();
  5. I2C Transaction types

    main

    The Transaction struct models the following I2C operations:

    TypeDescription
    WriteExpects an I²C write command with the wrapped bytes.
    ReadExpects an I²C read command and will return the wrapped bytes.
    WriteReadExpects an I²C write_read command where the expected bytes are written and the response bytes are returned.
  6. How SPI transactions and expectations work together

    main

    The SPI mock operates on a sequence of expectations. When a driver calls an SPI method (like write, transfer, or send), the mock:

    1. Retrieves the next Transaction from the expectation list.
    2. Validates that the Mode of the call matches the expected_mode in the transaction.
    3. Validates that the data being sent matches expected_data.
    4. If it is a transfer or read, it populates the driver's buffer with the response data defined in the transaction.

    If the driver calls a method when no expectations are left, or if the data/mode doesn't match, the mock will panic. Calling .done() at the end of a test verifies that all provided expectations were consumed.

  7. Mock digital I/O for embedded-hal 1.x

    main

    The embedded_hal_mock::eh1::digital module provides mock implementations for InputPin, OutputPin, StatefulOutputPin, and (if embedded-hal-async is enabled) the Wait trait.

    To use these mocks, you define a sequence of Transaction objects representing the expected interactions with the pin. These expectations are passed to Mock::new(&expectations). The mock will verify that every call made to the pin matches the expected transaction in order. If a call does not match or if there are remaining expectations after the test, the mock will panic or fail validation.

    Key lifecycle methods:

    • pin.done(): Validates that all expected transactions have been consumed.
    use embedded_hal::digital::{InputPin, OutputPin, StatefulOutputPin, ErrorKind};
    use embedded_hal_mock::eh1::digital::{Mock as PinMock, State as PinState, Transaction as PinTransaction};
    
    let err = ErrorKind::Other;
    
    // Configure expectations
    let expectations = [
        PinTransaction::get(PinState::High),
        PinTransaction::get(PinState::High),
        PinTransaction::set(PinState::Low),
        PinTransaction::set(PinState::High).with_error(err.clone()),
        PinTransaction::get_state(PinState::High),
        PinTransaction::toggle(),
        PinTransaction::get_state(PinState::Low),
    ];
    
    // Create pin
    let mut pin = PinMock::new(&expectations);
    
    // Run and test
    assert_eq!(pin.is_high().unwrap(), true);
    assert_eq!(pin.is_low().unwrap(), false);
    
    pin.set_low().unwrap();
    pin.set_high().expect_err("expected error return");
    
    pin.is_set_high().unwrap();
    pin.toggle().unwrap();
    pin.is_set_low().unwrap();
    
    pin.done();
    
    // Update expectations
    pin.update_expectations(&[]);
    pin.done();
  8. Mock SPI implementation for embedded-hal 1.x

    main

    The Mock<W> type provides a way to simulate SPI hardware for automated testing of drivers. It works by specifying a sequence of expected Transaction<W> objects. When the driver performs SPI operations (like read, write, or transfer), the mock verifies that the operation matches the next expected transaction in the sequence. If a mismatch occurs (wrong data, wrong mode, or unexpected call), the mock will trigger a runtime assertion to help locate the fault.

    To ensure all expected transactions were actually executed, you must call spi.done() at the end of your test.

    # use eh1 as embedded_hal;
    use embedded_hal::spi::SpiBus;
    use embedded_hal_mock::eh1::spi::{Mock as SpiMock, Transaction as SpiTransaction};
    
    // 1. Define expectations
    let expectations = [
        SpiTransaction::write(0x09),
        SpiTransaction::read(0x0A),
    ];
    
    // 2. Initialize the mock
    let mut spi = SpiMock::new(&expectations);
    
    // 3. Run your driver code (which uses SpiBus/SpiDevice)
    SpiBus::write(&mut spi, &[0x09]).unwrap();
    assert_eq!(SpiBus::read(&mut spi, &mut [0u8; 1]).unwrap(), Ok([0x0A]);
    
    // 4. Verify all expectations were met
    spi.done();
  9. How to test drivers using mocks

    main

    To test a driver in CI without physical hardware, use the mock implementations provided by this crate. The workflow follows these four steps:

    1. Define expectations: Create a list of transactions (e.g., specific read or write operations) that you expect the driver to perform on the hardware.
    2. Instantiate the mock: Create the mock object using those expectations.
    3. Run the test: Execute the driver code that uses the mock.
    4. Verify expectations: Call the .done() method on the mock instance at the end of your test to ensure every expected transaction actually occurred.

    Note on Cloning: Mocks use an Arc internally and can be cloned freely. You can clone a mock before passing it into your driver, allowing you to call .done() on the clone to verify expectations without needing to reclaim the original instance from the driver.

    // Conceptual workflow:
    // 1. Define expectations
    // 2. Instantiate mock
    // 3. Run driver
    // 4. mock.done()
  10. Use Async I2C Mock

    main

    If the embedded-hal-async feature is enabled, the Mock implementation also implements the embedded_hal_async::i2c::I2c trait. This allows you to test asynchronous drivers using the same expectation-based pattern as the synchronous version.

    // Requires feature = "embedded-hal-async"
    #[tokio::test]
    async fn async_impls() {
        use embedded_hal_async::i2c::I2c;
        use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as I2cTransaction};
    
        let expectations = [
            I2cTransaction::read(0xaa, vec![1, 2]),
            I2cTransaction::write(0xaa, vec![10, 12]),
        ];
        let mut i2c = I2cMock::new(&expectations);
    
        let mut buf = vec![0; 2];
        I2c::read(&mut i2c, 0xaa, &mut buf).await.unwrap();
        assert_eq!(buf, vec![1, 2]);
    
        i2c.done();
    }
  11. Choose a Delay implementation

    main

    Depending on your testing requirements, choose one of the following three delay implementations for embedded-hal 1.x:

    • NoopDelay: A stub that returns immediately without any actual delay. Use this for fast tests where hardware timing is not critical.
    • StdSleep: Uses std::thread::sleep to provide real delay behavior. Use this when your tests need to simulate actual time passing.
    • CheckedDelay: A mock that allows you to specify a sequence of expected delay transactions. By default, it does not perform actual delays, but you can enable real delays for specific transactions. This is ideal for verifying that a driver calls delays with the correct durations and types.
    # use eh1 as embedded_hal;
    use std::time::Duration;
    
    use embedded_hal::delay::DelayNs;
    use embedded_hal_mock::eh1::delay::{CheckedDelay, NoopDelay, StdSleep, Transaction};
    
    // No actual delay
    let mut delay = NoopDelay::new();
    delay.delay_ms(50); 
    
    // Real delay
    let mut delay = StdSleep::new();
    delay.delay_ms(50); 
    
    // Configurable mock with expectations
    let transactions = vec![
        Transaction::delay_ns(50_000_000),
        Transaction::delay_us(60_000),
        Transaction::delay_ms(70).wait(), // .wait() enables real delay for this transaction
    ];
    
    let mut delay = CheckedDelay::new(&transactions);
    
    delay.delay_ms(50);
    delay.delay_ms(60);
    delay.delay_ms(70); 
    delay.done();
  12. Use the I2C Mock for embedded-hal 1.x

    main

    The Mock implementation for I2C allows you to specify a sequence of expected transactions (reads, writes, etc.) and verify that your driver interacts with the I2C bus exactly as expected.

    To use it:

    1. Define an array of Transaction objects representing the expected sequence of operations.
    2. Initialize the mock using Mock::new(&expectations).
    3. Execute your driver code using the mock.
    4. Call i2c.done() at the end of your test to ensure all expected transactions were actually performed. If any transactions were left unconsumed, the test will panic.
    # use eh1 as embedded_hal;
    use embedded_hal::i2c::{ErrorKind, I2c, Operation};
    use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as I2cTransaction};
    
    // Configure expectations
    let expectations = [
        I2cTransaction::write(0xaa, vec![1, 2]),
        I2cTransaction::read(0xbb, vec![3, 4]),
    ];
    let mut i2c = I2cMock::new(&expectations);
    
    // Writing
    i2c.write(0xaa, &vec![1, 2]).unwrap();
    
    // Reading
    let mut buf = vec![0; 2];
    i2c.read(0xbb, &mut buf).unwrap();
    assert_eq!(buf, vec![3, 4]);
    
    // Finalise expectations
    i2c.done();