At the time of writing this, I couldn’t find any useful examples for mocking an actix actor.
Google and Github code search were useless.

Turns out I had to study the code on how to do it:

https://github.com/actix/actix/blob/6c5c25da7a77cc9b372c28352a69a64f7f05231e/src/actors/mocker.rs

This part:

//! Mocking is intended to be achieved by using a pattern similar to

//! #[cfg(not(test))]
//! type DBClientAct = DBClientActor;
//! #[cfg(test)]
//! type DBClientAct = Mocker<DBClientActor>;

gives a hint on how mocking an actor could be achieved.

Example

Lets build a http server that listens on ping and responds with pong.

Singleton actor processes ping/pong messages

As a result of using a singleton actor, all incoming messages are blocked until message processing is completed. Singularity is achieved by implementing the SystemService trait.

actors.rs

#[derive(Debug)]
pub struct PingerActor {}

impl Default for PingerActor {
    fn default() -> Self {
        Self {}
    }
}

impl Actor for PingerActor {
    type Context = Context<Self>;
}

impl Supervised for PingerActor {
    fn restarting(&mut self, _: &mut Self::Context) {
        println!("Restarting...")
    }
}

impl SystemService for PingerActor {}

impl Handler<Ping> for PingerActor {
    type Result = Result<Pong, Error>;

    fn handle(&mut self, _: Ping, _: &mut Context<Self>) -> Self::Result {
        println!("Got ping!");
        Ok(Pong {})
    }
}

Was singleton pattern needed in this example?

No. For my real use case, I needed to use a router actor that stores a state and dispatches messages to its workers. Singleton actor seemed like a good choice.

Web part

Let’s configure a http server on port 8080. To use our pinger actor, we’ll inject its address into actix. Consuming the pinger actor is as simple as providing it as an argument in the handler method (see handlers::deliver signature).

main.rs

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .data(PingerActor::from_registry())
            .service(
                web::scope("/")
                    .route("ping", web::get().to(handlers::deliver::<PingerActor>))
            )
    })
        .bind("127.0.0.1:8080")?
        .run()
        .await
}

Route handler accepts address of an actor that handles Ping messages.
handlers.rs

pub async fn deliver<A>(actor: web::Data<Addr<A>>) -> Result<HttpResponse, Error>
    where A: Actor + Handler<Ping>,
          A::Context: ToEnvelope<A, Ping>, {
    let actor_request = actor.send(Ping {});

    let maybe_result = actor_request.await;

    maybe_result
        .map_err(|mailbox_err| ErrorInternalServerError(mailbox_err))?
        .map_err(|io_error| ErrorInternalServerError(io_error))
        .map(|_| HttpResponse::Ok().body("Pong"))
}

Test

Below is a unit test scenario for a handler’s deliver method:

handler_tests.rs

#[cfg(test)]
mod tests {
    use actix::actors::mocker::Mocker;
    use actix::{Addr, Actor};
    use actix_web::http::StatusCode;
    use crate::handlers;
    use crate::actors::PingerActor;
    use actix_web::web;
    use actix_web::body::Body;
    use crate::messages::*;

    pub type PingerActorMock = Mocker<PingerActor>;

    #[actix_rt::test]
    async fn test_deliver(){

        let mocker = PingerActorMock::mock(Box::new(move |_msg, _ctx| {
            let result: Result<Pong, std::io::Error> = Ok(Pong{});
            Box::new(Some(result))
        }));

        let actor: Addr<PingerActorMock> = mocker.start();

        let result = handlers::deliver::<PingerActorMock>(web::Data::new(actor)).await;

        assert!(result.is_ok());

        let http_result = result.unwrap();

        assert_eq!(http_result.status(), StatusCode::OK);

        let result_body = body_as_text(http_result.body().as_ref().unwrap());

        assert_eq!(result_body, "Pong");
    }

    fn body_as_text(body: &Body) -> String {
        match body {
            Body::Bytes(bytes) => {
                String::from_utf8(bytes.to_vec()).expect("Cannot convert bytes to string")
            },
            _ => panic!("Invalid result")
        }
    }

}

Note that you don’t need to start an entire actix service, then do a http request just to test a route handler method.

You don’t need to instantiate an actual implementation of the actor.
If your test runs a 1000x it should produce a same outcome.

In this case maybe its irrelevant if you provide a mock vs impl actor address.

But what if your actor has some internal i/o logic?
I/o can cause defects and affect the outcome of your test.

Full code is available on github. If you have a suggestion for unit test, comment below.


0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *