How do I create a mock function in Rust?

It would help to also see a minimal implementation of root_reducer. Something like this, I imagine?

fn root_reducer(state: State, action: Action) -> State {
    match action {
        Action::Status(mut status_function) => status_function(state),
        _ => state,
    }
}

The second error, borrow of moved value: 'state' is easily explained here; The root_reducer consumes (moves ownership of) state, and then the assert_eq tries to access the moved variable. This is why I said earlier you might need to implement Clone. This is a good tool if you need to move a value but then access a copy of that value later.

  1. Add Clone to the derive line on State
  2. Move a clone: assert_eq!(root_reducer(state.clone(), ...), state);

The first and third errors are actually related. Because you are boxing the closure, everything it references must live for 'static. This means that the status_mock_called variable must outlive the function. That clearly doesn't happen here, because it's stack allocated and gets freed when the function returns. Two ways to solve this:

  1. If you need to continue using a boxed closure, you can move a reference counted pointer into it. Even though the stack-allocated Rc will still be freed as normal, the cloned Rc owned by the closure will be allowed to live for 'static.

    This is certainly a bit more involved, requiring interior mutability. Because of the interior mutability, the closure can be Fn, but must move its environment. Anyway here's an example.

  2. The boxing may be unnecessary. And in this simple case, it can be removed! This will allow the reference to live only as long as the closure, on the stack. It's also significantly less complex than the previous example with Rc.

    And here's an example without boxing.

1 Like

I'm having problems with those generic types. Rust is complaining about root_reducer and Action sometimes not getting their type argument.

I can't see the code or error messages from where I'm sitting. ¯\_(ツ)_/¯

If you're not able to get the generic version working (and it may be impossible in much more complex scenarios than shown in the playground link) then you can try the Rc<Cell<T>> approach. It you are sending the closure across threads, these types will have to be changed to Arc<Mutex<T>>.

Alright, I'll try the other approach. Here is my much longer code (with several compiler errors cause I'm kind of a mess right now :sweat_smile:). I would stop and do some exercises in borrowing and lifetimes and all that stuff, but I'm kind of on a deadline :grimacing:.

use chrono::prelude::*;
use chrono::Duration;
use std::boxed::Box;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct State {
    date: Date<Utc>,
    miles: u64,
    food: u64,
    health: u64,
    hunt_days: i64,
}

enum Action<T>
where
    T: FnMut(State) -> State,
{
    Hunt,
    Travel(Duration, u64),
    Rest(Duration),
    Status(T),
}

/// The main function that uses an Action to get a new State
///
/// # Examples
/// ```
/// root_reducer(
///   State {
///     date: Utc.ymd(2020, 3, 1),
///     miles: 2000,
///     food: 500,
///     health: 5,
///     hunt_days: 2,
///     rest_days: 2,
///     travel_days: 3,
///     travel_distance: 30,
///   },
///   Action::Travel
/// )
/// ```
fn root_reducer<T>(state: State, action: Action<T>) -> State
where
    T: FnMut(State) -> State,
{
    match action {
        Action::Travel(days, distance) => State {
            date: state.date + days,
            miles: state.miles - distance,
            ..state
        },
        Action::Rest(days) => State {
            date: state.date + days,
            // No more than 5 health
            health: if state.health < 5 {
                state.health + 1
            } else {
                state.health
            },
            ..state
        },
        Action::Hunt => State {
            date: state.date + Duration::days(state.hunt_days),
            food: state.food + 100,
            ..state
        },
        Action::Status(mut status_function) => status_function(state),
    }
}

fn main() {
    println!("Hello World!");
}

#[cfg(test)]

mod tests {
    use super::*;

    #[test]
    fn test_travel() {
        let default_state = State {
            date: Utc.ymd(2020, 3, 1),
            miles: 2000,
            food: 500,
            health: 5,
            hunt_days: 2,
        };

        let travel_result_state = State {
            miles: 1970,
            date: Utc.ymd(2020, 3, 4),
            ..default_state
        };
        let travel_result_state_with_more_days: State = State {
            date: Utc.ymd(2020, 3, 5),
            ..travel_result_state
        };
        let travel_result_state_with_more_miles: State = State {
            miles: 1960,
            ..travel_result_state
        };

        let default_duration = Duration::days(3);
        let longer_duration = Duration::days(4);
        let default_distance = 30;
        let longer_distance = 40;

        let default_action = Action::Travel(default_duration, default_distance);

        assert_eq!(
            root_reducer(default_state, default_action),
            travel_result_state
        );

        assert_eq!(
            root_reducer(
                default_state,
                Action::Travel(longer_duration, default_distance)
            ),
            travel_result_state_with_more_days
        );
        assert_eq!(
            root_reducer(
                default_state,
                Action::Travel(default_duration, longer_distance)
            ),
            travel_result_state_with_more_miles
        );
    }

    #[test]
    fn test_rest() {
        let default_state = State {
            date: Utc.ymd(2020, 3, 1),
            miles: 2000,
            food: 500,
            health: 5,
            hunt_days: 2,
        };
        let rest_default_state = State {
            health: 4,
            ..default_state
        };
        let default_duration = Duration::days(2);

        assert_eq!(
            root_reducer(rest_default_state, Action::Rest(default_duration)),
            default_state
        );
        assert_eq!(
            root_reducer(default_state, Action::Rest(default_duration)),
            default_state
        );
    }
    #[test]
    fn test_hunt() {
        let default_state = State {
            date: Utc.ymd(2020, 3, 1),
            miles: 2000,
            food: 500,
            health: 5,
            hunt_days: 2,
        };
        let hunt_state_with_more_days: State = State {
            hunt_days: 3,
            ..default_state
        };
        let hunt_result_state: State = State {
            food: 600,
            ..default_state
        };
        let hunt_result_state_with_more_days = State {
            date: Utc.ymd(2020, 3, 4),
            ..hunt_result_state
        };

        assert_eq!(root_reducer(default_state, Action::Hunt), hunt_result_state);

        assert_eq!(
            root_reducer(hunt_state_with_more_days, Action::Hunt),
            hunt_result_state_with_more_days
        );
    }

    #[test]
    fn test_status() {
        let default_state = State {
            date: Utc.ymd(2020, 3, 1),
            miles: 1970,
            food: 500,
            health: 5,
            hunt_days: 2,
        };
        let mut status_mock_called = false;

        let status_mock = |state: State| -> State {
            status_mock_called = true;
            return state;
        };
        assert_eq!(
            root_reducer(default_state, Action::Status(status_mock)),
            default_state
        );

        assert!(status_mock_called);
    }
}

Ah, yes. That is one of the challenges with generic enums; the compiler needs to know the type of T, even if the variant isn't using it directly. FWIW you will hit the same problem if you try passing None to a function that accepts Option<T>.

The trick here is using TurboFish syntax to set an explicit type for T:

Action::<Box<dyn FnMut(State) -> State>>::Hunt

Using this everywhere you want to create a Hunt variant adds a lot of line noise, though. This can be abstracted away by creating a new enum that doesn't have the variant which owns T, and implement std::convert::From for it:

enum SimpleAction {
    Hunt,
    Travel(Duration, u64),
    Rest(Duration),
}

impl From<SimpleAction> for Action<Box<dyn FnMut(State) -> State>> {
    fn from(action: SimpleAction) -> Self {
        use SimpleAction::*;

        match action {
            Hunt => Action::<Box<dyn FnMut(State) -> State>>::Hunt,
            Travel(d, i) => Action::<Box<dyn FnMut(State) -> State>>::Travel(d, i),
            Rest(d) => Action::<Box<dyn FnMut(State) -> State>>::Rest(d),
        }
    }
}

Now you can create a Hunt variant with a lot less noise:

SimpleAction::Hunt.into()

Full example in the playground.

Also, a macro should be possible to build with the synstructure crate to keep the enum DRY.

1 Like

Here's my attempt playground, hope it helps. Figured I'd give it a shot since I've worked with react and redux before.
I removed the generic bound

T: FnMut(State) -> State

from enum Action and fn root_reducer.

The earlier error of

status_mock_called` does not live long enough

is fixed by a lifetime parameter on enum Action, so rustc can verify that the closure does not outlive fn test_status()

2 Likes