-
Notifications
You must be signed in to change notification settings - Fork 10
Allow mocking out requests #220
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR introduces client mocking capabilities for twirp-rs, allowing developers to test code with twirp clients without requiring a running HTTP server. The implementation generates mock handlers for RPC traits and provides a MockClientBuilder for easy test setup.
- Adds a new
mock.rs
module with request/response encoding/decoding utilities - Implements
MockHandler
trait andMockClientBuilder
for creating test clients with mock service handlers - Generates mock client structs automatically via the twirp-build code generation
Reviewed Changes
Copilot reviewed 8 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
File | Description |
---|---|
crates/twirp/src/mock.rs | New module providing mock request/response utilities |
crates/twirp/src/client.rs | Enhanced client with mock handler support and MockClientBuilder |
crates/twirp-build/src/lib.rs | Code generation for mock client structs |
crates/twirp/src/error.rs | Added rust error chaining to error conversions |
example/proto/status/v1/status_api.proto | New example proto file for testing |
example/Cargo.toml | Added dependencies for mock support |
Makefile | Enhanced linting configuration |
crates/twirp/src/client.rs
Outdated
} | ||
#[cfg(any(test, feature = "test-support"))] | ||
impl MockHandlers { | ||
#[allow(dead_code)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The #[allow(dead_code)]
attribute suggests this function may not be used. Consider removing it if it's truly unused or documenting its intended usage.
#[allow(dead_code)] | |
#[allow(dead_code)] | |
/// Creates a new instance of `MockHandlers`. | |
/// | |
/// This method is intended for use in testing or mock environments | |
/// where mock handlers need to be set up for specific hosts and services. |
Copilot uses AI. Check for mistakes.
crates/twirp/src/client.rs
Outdated
} | ||
} | ||
|
||
#[allow(dead_code)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The #[allow(dead_code)]
attribute suggests this function may not be used. Consider removing it if it's truly unused or documenting its intended usage.
#[allow(dead_code)] | |
#[allow(dead_code)] | |
/// Adds a mock handler for a specific host and service. | |
/// | |
/// This function is intended for use in testing scenarios or when the `test-support` | |
/// feature is enabled. It allows associating a mock handler with a host/service pair | |
/// for use in mock request handling. |
Copilot uses AI. Check for mistakes.
crates/twirp/src/mock.rs
Outdated
// TODO: Mock out other headers and extensions in the request and response? | ||
|
||
// NOTE: For testing and mocking only. | ||
pub async fn decode_request<I>(mut req: reqwest::Request) -> Result<Request<I>> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it is easiest to go through serialization/deserialization...
In theory it should be possible to short-cut this by adding some mocking functionality to the service implementations of the twirp Client (I think).
But that's kind of an "optimization" which we can look into, once we have everything working, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it is easiest to go through serialization/deserialization...
Yes. This the easiest for sure if we want to allow calling code to just use a twirp::Client
.
I agree that it would be nice to optimize further in the future.
example/src/bin/client.rs
Outdated
#[tokio::test] | ||
async fn test_client_with_mock() { | ||
let client = MockClientBuilder::new() | ||
.with_mock(MockHaberdasherApiClient::new(Mock)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: the Mock
clients are simply forwarding implementations, correct?
If so, would it make sense to rename them?
For an actual Mock
implementation, I would expect that I can do more stuff, like registering functions for individual RPCs or so. Not saying that we have to add such a feature right now, but just wondering whether the Mock
term should be replaced by something more specific.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is a good point. 🤔 They are really just a shim to pass data to whatever struct implements the rpc trait... Let me play with some other names here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like it!
|
||
#[async_trait] | ||
pub trait DirectHandler: 'static + Send + Sync { | ||
fn service(&self) -> &str; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: Consider making this method name more explicit, to avoid confusion that it may have something to do with the Service
struct or tower::Service
.
fn service(&self) -> &str; | |
fn service_name(&self) -> &str; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to leave this as-is because it is an internal detail and for use client side (so no confusion with tower).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know about the naming. I guess "direct" is the new name? "Mock" communicates what we're doing better though. (Edit:) OK, I see Alex's comment. I don't have a better suggestion.
I noticed even ClientBuilder::direct()
doesn't let you build a Client
that simply never talks to the network. If an unexpected .with_host
happens, you'll end up attempting HTTP requests. Our integration tests for Blackbird really don't want that. I guess it shouldn't happen in practice. But if it does, we're not making it easy on ourselves to debug it.
let mut method_matches = Vec::with_capacity(service.methods.len()); | ||
for m in &service.methods { | ||
let name = &m.name; | ||
let method = &m.proto_name; | ||
method_matches.push(quote! { | ||
#method => { | ||
twirp::details::encode_response(self.inner.#name(twirp::details::decode_request(req).await?).await?) | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style suggestion: Use iterators and unpacking of the Method:
let mut method_matches = Vec::with_capacity(service.methods.len()); | |
for m in &service.methods { | |
let name = &m.name; | |
let method = &m.proto_name; | |
method_matches.push(quote! { | |
#method => { | |
twirp::details::encode_response(self.inner.#name(twirp::details::decode_request(req).await?).await?) | |
} | |
}); | |
let method_matches = service.methods | |
.iter() | |
.map(|Method { name, proto_name, .. }| quote! { | |
#proto_name => { | |
twirp::details::encode_response(self.inner.#name(twirp::details::decode_request(req).await?).await?) | |
} | |
}) | |
.collect::<Vec<_>>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to leave this using the more imperative style to be consistent with the rest of the code gen.
crates/twirp/src/client.rs
Outdated
@@ -54,6 +103,7 @@ pub struct Client { | |||
http_client: reqwest::Client, | |||
inner: Arc<ClientRef>, | |||
host: Option<String>, | |||
handlers: Option<RequestHandlers>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be inside the ClientRef
so that it's reference counted. That saves cloning the hash map in with_host
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
I don't either... passthrough? forwarding? request handlers? @aneubeck is correct that these aren't true mocks, though the thing you pass in that implements the rpc trait is close. I think we're also realizing that these have potential uses beyond testing/mocking: e.g. when you want to use the twirp rpc traits as a contract, but co-locate some of the distributed system and just have a method call instead of an http call.
Good point. I just made an attempt to fix this as well, let me know if you think that helps. |
Introduces the ability to register (client) mocks for generated rpc traits. This allows testing code with twirp clients where you don't have an http server running (and don't want to otherwise mock one out).
You can see an example usage in
example/src/bin/client.rs
.I still have a little work to do in getting all the cargo features sorted out. My goal is that you can opt-in to all this with a standard feature flag, but otherwise this code won't be in your runtime binaries.
This is possible because of the work in #212.