How to write test async functions without using trait objects

I'm hitting a similar error. (This was split off from Problems caused by async trait topic.)

I was anxiously awaiting async fn in Traits so I could use them to write test implementations for async functions. I see now that my solution isn't going to work, because it uses dynamic dispatch and a non-object-safe trait (I'm just now hearing of these concepts in the context of Rust).

Here's what I'm trying to do:

I have several functions that are async. They weren't even in a Trait by the way. Those functions make calls over the internet. Obviously in unit tests, I don't want to make real calls over the internet. I also don't want to just mock out the calls. I want to write a test implementation that can mimic the behavior of the real implementation.

My approach was to put the async functions in a Trait, and have a production implementation of the Trait that goes over the internet, and a test implementation of the Trait that keeps state on disk or in memory.

But that had me passing Arc all over. And ultimately, that gave me the same error message listed in this post: error[E0391]: cycle detected when checking effective visibilities

What is the correct way to write fake implementations of async functions if not with Traits?

Welcome @rust_amateur! I split this off from the Problems caused by async trait topic because it already had a solution and I suspect your question is going a different direction. I hope that's ok! Please edit the title if it doesn't describe your question.

1 Like

I am not proficient with async myself. But I was wondering if you had seen this async book chapter on using traits for testing. There is also this page in the tokio tutorial that has a section on Mocking using AsyncRead and AsyncWrite.

Have you tried creating two implementations of your trait (testing and production) without using trait objects, i.e., without using dyn? If that didn't work out, what problems did you encounter that led you to using trait objects?

Obviously it is more convenient to pass in a trait object than to use generic params in multiple places. So perhaps a better question is whether these traits could be made object safe.

If you're sure you do need trait objects, and it is not practical for the trait to be object safe, see @steffahn's solution in the earlier thread. (I assumed you had already seen it since you posted there.)

Thank you for responding (and splitting off the question)!

So, a couple responses:

Regarding the chapter you referenced in the async book, that's not really going to achieve what I'm trying to do.

Here's why I'm using trait objects: We have a lot of existing functions that pass around a "client" struct (from a 3rd party crate) as an argument, where at the very lowest level, our code uses that client to make calls over the internet. In order to write higher level (system) tests that call through those functions (as opposed to just unit testing a single function), I wanted to instantiate a fake/dummy client and pass that instead. That way I could basically write unit tests that are system tests; that test the whole app almost.

As for the mock example, it just won't help me in this case.

As for @steffahn's solution to the earlier thread, I haven't tried async_trait. I did try passing Arc<dyn Client> (where Client is the trait I created with async functions) into functions that previously took just a real client. That attempt is what gave me the error about the circular dependencies.

At this point, I think I'm going to can the entire attempt to use a Trait at all, and instead just use #[cfg(not(test))] and #[cfg(test)] to write dummy implementations. (Luckily, I don't need to support two different clients with independent state; the same client is passed around everywhere.)

I think I understand. I assume:

  1. The client struct implements a trait, and you use that trait as your client API. The trait has async methods, which is what prevents you from using trait objects to pass in a test client.
  2. You use or pass that client trait in a lot of places, so you don't want to change to use a different trait that doesn't have async methods and wraps the first trait (one of Steffahn's suggestions). Sorry, this isn't possible because you need async methods.

The async_trait crate could solve the problem #1, except that the trait itself and its impl must be annotated with #[async_trait], which you can't do because it's in a 3rd party crate.

However, you could try creating a client trait of your own with the same methods, annotate it with #[async_trait], and implement it's methods by forwarding to the original client methods. This is a combination of the suggestions that Steffahn posted. It's just a thought. I haven't tried it myself and I could be making an incorrect assumption.

That's understandable. It's simple and I don't see anything wrong with it, since you only need the special client for testing.

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.