Mocking tokio, hyper and reqwest without using trait or changing production code by using injectorpp

⚓ rust    📅 2025-07-05    👤 surdeus    👁️ 2      

surdeus

Hi,

We have recently added tests in injectorpp to demonstrate how to fake tokio, hyper and reqwest requests without using trait or changing production code. See tokio.rs, hyper.rs and reqwest.rs

Since reqwest uses hyper, hyper is built on top of tokio, The basic steps are all the same:

  • Create a mock TcpStream.
  • Fake dns function to make it always success.
  • Fake TcpSocket::connect to return the mock TcpStream.
  • If it's a https request, fake Uri::scheme_str to make it always return http to bypass all tls validation.

We can take a breakdown for reqwest.rs.

Create a mock TcpStream

TcpStream cannot be directly created as it does not provide public constructor. make_tcp_with_json_response is an example to write the expected contents to a temporary listen port to get a TcpStream object.

Fake dns function to make it always success

Our goal is sending to any host should get the mock response we set even if that host does not exist. So we need to fake the to_socket_addr function:

    type ToSocketAddrsFn =
        fn(&(&'static str, u16)) -> std::io::Result<std::vec::IntoIter<SocketAddr>>;
    let fn_ptr: ToSocketAddrsFn = <(&'static str, u16) as ToSocketAddrs>::to_socket_addrs;

    unsafe {
        injector
            .when_called_unchecked(injectorpp::func_unchecked!(fn_ptr))
            .will_execute_raw_unchecked(injectorpp::closure_unchecked!(
                |_addr: &(&str, u16)| -> std::io::Result<std::vec::IntoIter<SocketAddr>> {
                    Ok(vec![SocketAddr::from(([127, 0, 0, 1], 0))].into_iter())
                },
                fn(&(&str, u16)) -> std::io::Result<std::vec::IntoIter<SocketAddr>>
            ));
    }

Fake TcpSocket::connect to return the mock TcpStream

Now it's time to fake the TcpSocket::connect to return the contents we expect in the test:

    let temp_socket = TcpSocket::new_v4().expect("Failed to create temp socket");
    let temp_addr = "127.0.0.1:0".parse().unwrap();

    // Mock TcpSocket::connect method
    injector
        .when_called_async(injectorpp::async_func!(
            temp_socket.connect(temp_addr),
            std::io::Result<TcpStream>
        ))
        .will_return_async(injectorpp::async_return! {
            make_tcp_with_json_response(),
            std::io::Result<TcpStream>
        });

If it's a https request, fake Uri::scheme_str to make it always return http to bypass all tls validation

https needs additional tls validation and it's one of the tough challenges for testing the code that sends requests. But now it's easy to bypass it by faking Uri::scheme_str:

    // Force using http to bypass tls validation
    injector
        .when_called(injectorpp::func!(fn (Uri::scheme_str)(&Uri) -> Option<&str>))
        .will_execute(injectorpp::fake!(
            func_type: fn(_uri: &Uri) -> Option<&str>,
            returns: Some("http")
        ));

Now sending any https request to any host will return the contents you set.

Hope this can help you solving the pain point for writing unit tests when using above libraries.

Please leave your suggestions and questions, we'd like to understand your pain point and to see if injectorpp can help. We're also considering wrapping an utility module to simplify some steps when faking these libraries. Please do let us know your thoughts. Thanks!

1 post - 1 participant

Read full topic

🏷️ rust_feed