How to test this async logic?

I wrote a small library for async detection of button events in a no_std environment: async-button. It's build around embedded-hal traits and embassy-time.

In my manual testing it works as intended but I would like to write some automated tests to be able to test edge cases that are difficult to trigger my manually pushing a button.

As you can see in lib.rs all the logic worth testing occurs in the update function. This probably makes it harder to test but I don't see a logical way to split it in smaller parts.

The logic heavily depends on timeouts and delays and I would need to somehow simulate that to trigger different outputs. How can I do that?

There are async functions that you can leverage to simulate timeouts. For example: DelayUs in embedded_hal_async::delay - Rust

What I ended up doing is using #[cfg(test)] to switch between embassy-time and equivalent tokio functions. I then used a modified version of embedded-hal-mock to mock the embedded-hal traits and artificially delay the async functions from embedded_hal_async::digital::Wait so it would trigger the timeouts. This leads to test code like this:

const IMMEDIATELY: Duration = Duration::from_millis(0);

#[tokio::test]
async fn double_click() {
    let expectations = [
        Transaction::wait_for_state(PinState::Low, IMMEDIATELY),
        Transaction::get(PinState::Low),
        Transaction::wait_for_state(PinState::High, IMMEDIATELY),
        Transaction::get(PinState::High),
        Transaction::wait_for_state(PinState::Low, IMMEDIATELY),
        Transaction::get(PinState::Low),
        Transaction::wait_for_state(PinState::High, IMMEDIATELY),
        Transaction::get(PinState::High),
        Transaction::wait_for_state(PinState::Low, Duration::from_millis(250)),
    ];
    let pin = Mock::new(&expectations);
    let mut button = Button::new(pin, CONFIG);
    button.state = State::Idle;

    let event = button.update_step().await;
    assert_none!(event);
    assert_matches!(button.state, State::Pressed);

    let event = button.update_step().await;
    assert_none!(event);
    assert_matches!(button.state, State::Released);

    let event = button.update_step().await;
    assert_none!(event);
    assert_matches!(button.state, State::Pressed);

    let event = button.update_step().await;
    assert_none!(event);
    assert_matches!(button.state, State::Released);

    let event = button.update_step().await;
    assert_some_eq!(event, ButtonEvent::ShortPress { count: 2 });
    assert_matches!(button.state, State::Idle);

    button.pin.done();
}

I'm not super happy about this approach because I find the test code pretty hard to understand. Also, implementation details of the test code leak into the main library code. I'd be happy to hear if there are better ways to do this this.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.