Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: How to mock SubsystemHandle for testing? #52

Closed
cemremengu opened this issue Jan 11, 2023 · 5 comments
Closed

Question: How to mock SubsystemHandle for testing? #52

cemremengu opened this issue Jan 11, 2023 · 5 comments

Comments

@cemremengu
Copy link

cemremengu commented Jan 11, 2023

Hello I am new to Rust and sorry if this is a very obvious question.

I am using this crate with the hyper example and was wondering if you had any suggestions for testing approach when using SubsystemHandle as a parameter to the run function. How would you mock this parameter during tests so that it will not be necessary to call Toplevel::new() everytime?

Thanks for your work!

@Finomnis
Copy link
Owner

Hmmm... I don't have anything built-in for this yet. I might have to introduce a trait that represents the SubsystemHandle so you can do a proper dependency injection.

Do those words mean anything to you? :) if not, I can explain.

Either way it might require changes to this library.

@cemremengu
Copy link
Author

Yes, I got the idea thanks for the help! I think it would be a good idea to provide such trait in general.

Once I have more experience, I would like to help with a PR for this :)

@Finomnis
Copy link
Owner

I gave it some thought, and I'm unsure why calling 'Toplevel::new' every time is a bad idea. It doesn't have any side effects and should be quite performant. I do it at my tests as well.

I tried coming up with a mocking interface and failed; async traits are not implemented yet, and even if we use something like the async-trait crate it would be unclear what exactly would have to be mocked, how calling the mock functions would have to behave, etc.

It might be easier to mock on your side, so you can control it yourself. Like this:

use std::{
    sync::{
        atomic::{AtomicU32, Ordering},
        Arc,
    },
    time::Duration,
};

use async_trait::async_trait;
use tokio_graceful_shutdown::{SubsystemHandle, Toplevel};

#[async_trait]
trait SubsystemHandleTrait {
    async fn on_shutdown_requested(&self);
}

#[async_trait]
impl SubsystemHandleTrait for SubsystemHandle {
    async fn on_shutdown_requested(&self) {
        SubsystemHandle::on_shutdown_requested(self).await;
    }
}

struct MySubsystem {
    counter: Arc<AtomicU32>,
}
impl MySubsystem {
    pub async fn run(self, subsys: impl SubsystemHandleTrait) -> Result<(), String> {
        self.counter.fetch_add(1, Ordering::Relaxed);
        log::info!("Started.");
        subsys.on_shutdown_requested().await;
        self.counter.fetch_add(1, Ordering::Relaxed);
        log::info!("Stopped.");
        Ok(())
    }
}

#[tokio::main]
async fn main() {
    // Init logging
    use env_logger::{Builder, Env};
    Builder::from_env(Env::default().default_filter_or("debug")).init();

    let counter = Arc::new(AtomicU32::new(0));

    Toplevel::new()
        .catch_signals()
        .start("MySubsys", {
            let counter = Arc::clone(&counter);
            move |subsys| MySubsystem { counter }.run(subsys)
        })
        .handle_shutdown_requests(Duration::from_secs(2))
        .await
        .unwrap();

    log::info!("Counter: {:?}", counter);
}

#[cfg(test)]
mod tests {
    use super::*;

    use core::future::Future;
    use mockall::mock;

    mock! {
        MockedSubsystemHandle {}
        impl SubsystemHandleTrait for MockedSubsystemHandle {
            fn on_shutdown_requested<'b: 'a, 'a>(&'a self) -> impl Future<Output = ()> + core::marker::Send + 'a;
        }
    }

    #[tokio::test]
    async fn counter_gets_incremented_on_shutdown() {
        // Arrange
        let counter = Arc::new(AtomicU32::new(0));
        let subsystem = {
            let counter = Arc::clone(&counter);
            MySubsystem { counter }
        };

        let (shutdown_sender, shutdown_receiver) = tokio::sync::oneshot::channel::<()>();
        let mut subsys_handle = MockMockedSubsystemHandle::new();
        subsys_handle
            .expect_on_shutdown_requested()
            .times(1)
            .return_once(|| {
                Box::pin(async {
                    shutdown_receiver.await.unwrap();
                })
            });
        let running_subsystem = tokio::spawn(subsystem.run(subsys_handle));
        tokio::time::sleep(Duration::from_millis(50)).await;

        // Act & Assert
        assert_eq!(counter.load(Ordering::Relaxed), 1);
        shutdown_sender.send(()).unwrap();
        tokio::time::sleep(Duration::from_millis(50)).await;
        assert_eq!(counter.load(Ordering::Relaxed), 2);
        running_subsystem.await.unwrap().unwrap();
    }
}
[2023-01-20T23:34:20Z INFO  rust_tmp] Started.
^C[2023-01-20T23:34:21Z DEBUG tokio_graceful_shutdown::signal_handling] Received SIGINT.
[2023-01-20T23:34:21Z INFO  tokio_graceful_shutdown::shutdown_token] Initiating shutdown ...
[2023-01-20T23:34:21Z INFO  rust_tmp] Stopped.
[2023-01-20T23:34:21Z DEBUG tokio_graceful_shutdown::toplevel] Shutdown successful. Subsystem states:
[2023-01-20T23:34:21Z DEBUG tokio_graceful_shutdown::toplevel]     MySubsys  => OK
[2023-01-20T23:34:21Z INFO  rust_tmp] Counter: 2
running 1 test
test tests::counter_gets_incremented_on_shutdown ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s

@Finomnis
Copy link
Owner

@cemremengu Is this sufficient for you so I can close this issue? Or do you still require changes to the crate?

@cemremengu
Copy link
Author

Yes, thank you so much for this! Sorry, I was about the write reply but due to my internet connection I could only drop a heart :)

Thanks again for your work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants