faux
faux copied to clipboard
Generics in mocked methods
Neither of these work.
pub fn new_generic_input<T>(&self, a: T) { /* some code here */ }
pub fn new_generic_output<T>(&self) -> T { /* some code here */ }
pub fn impl_arg(&self, a: impl SomeTrait) { /* some code here */ }
pub fn impl_out(&self) -> impl SomeTrait { /* some code here */ }
The issue is on the definition of the _when_{}
method.
The first two seem solvable together. Add all the generics from the mocked method to the when
method.
The third one seems like it would require "de-sugaring" the impl Foo
into <T: Foo>
for the when
method only.
The last one.... :man_shrugging: We could change it into a generic argument to the when
method as a first-step MVP (read: hack).
I am actually running into this with code similar to this:
pub fn list_directory(&self, ... callback: impl fn(files::Metadata)>
Is there a known workaround how I can mock this code still?
The only workaround I can think of is not mocking this method, but mocking things that this method calls.
#[faux::methods]
impl MyStruct {
// this can be mocked
pub fn metadata(&self) -> files::Metadata {
/* get it somehow */
}
}
impl MyStruct {
// this cannot be mocked so it is in a separate impl block.
pub fn list_directory(&self, callback: impl fn(files::Metadata) {
// the call to metadata may have been mocked.
let metadata = self.metadata();
callback.call(metadata);
}
}
The main thing to note is that in the non-mocked methods, you may not access any fields in the struct. It is not very nice but at least it lets you get around the issue somewhat, and you can still mock the other methods on your struct.
Thanks for bringing up that you run into this! It helps me prioritize what to do next.
Ah, unfortunately that did not help me, since I really needed the callback based API in my use case. I went with pulling my API out into a trait and manually mock the behavior of this trade now. Good enough for my uses for me, but I eagerly await mock becoming more powerful!
Thanks for your work, this all seems super promising.
Would changing your signature to use generics rather than impl
arguments be good enough? The former is significantly easier from a faux
's perspective but I am aware that it is not as ergonomic to use.
Changing:
pub fn list_directory(&self, ... callback: impl Fn(files::Metadata))
to:
pub fn list_directory<F: Fn(files::Metadata)>(&self, ... callback: F)
I would not mind doing that for easier mocking in future cases - the loss of ergonomics in this case is minimal and the gain of direct mocking substantial, so totally worth it.
Apologies for the long turn around time, life has been a little crazy and this actually ended up being a bit of a mind melt. As it turns out it is actually easier for faux
to mock methods that use impl
rather than generic bounds. However, the caveat to this is that this only works for impl
arguments that are object safe.
My current solution (https://github.com/nrxus/faux/commit/a26cf098ce7104d3dbae7027b0d646983431d3ec) wraps all impl Trait
arguments in a Box<dyn Trait>
when calling for when!(..).*then()
. For callback methods, such as your own, Box<Fn*>
implements Fn*
so it should be fairly seamless, but it does make other use cases less ergonomic.
Unfortunately this is the only way I can think of at the moment to allow for this in a "safe" manner. I think I might just release this as-is to see if it solves your use case with the caveat that if I think of a better solution I might end up changing the API of it.
Implementation Issues (writing for my own sake, no need to read)
Unsafety
For methods such as:
pub fn my_method<T>(&self, a: T) {}
My first though for a when
method was:
fn _when_my_method<T>(&self) -> When<(T), ()> {
/* make a When */
}
The When
struct now has *then
methods that look like:
fn then(self, mock: impl FnMut(T) -> O + 'static + Send) {
/* store the mock */
}
At this point the user can now pass a mock that assumes any user-specified T:
mocked_struct._when_my_method<i32>().safe_then(|i: i32| {})
This seems all fine and dandy until... the user calls for the method:
mocked_struct.my_method("a string is still a T this method accepts");
And now we are big trouble. faux
is going to transmute stuff as it does assuming all the checks have been done, and it's going to try to pass the user-given &str
to a closure that accepts i32
, and because it's all transmuted it will create UB.
Unnameable types
Given a structure similar to above, how would someone declare a mock for:
pub fn my_callback<F: Fn(i32)>(&self, a: F) {}
When method:
fn _when_my_method<F: Fn(i32)>(&self) -> When<(T), ()> {
/* make a When */
}
When the user calls for when:
mocked_struct._when_my_method</* what goes here? */>()
.safe_then(|i: /* what type is this? */| {})
I don't believe there is a way to name concrete types to closures as generic bounds so.... this would not work.
Alternatives
Exclude and ignore generic types
Somehow (?) detect what arguments are generics, and exclude them from the When
method. This is fairly limiting but it could be "simple"
Box all generic types
Somehow (?) detect what arguments are generics, box them all up. This means make the When
methods receive a boxed argument, and when the method is called box the argument prior to passing it to the mock. This means that only bounds that can be converted into a Box<dyn Trait>
are allowed but it is better than nothing. There is an outstanding issue, however, how to deal with generics without a trait bound, should a dumb trait be added that anything implements? Perhaps use the Any
trait and only support 'static
types in generics? This is a pretty big open question =/
These two depend on finding what the trait arguments are which is not easy task for more complicated cases but it is perhaps doable, I would have to try it out. The impl Trait
case is simpler because I do not have to "find" them, they are fairly explicit in the arguments.
I clicked the wrong button, this is definitely not closed.
Thanks for you work! I definitely make use of this!
I think I'm hitting this with
#[cfg_attr(test, faux::methods)]
impl S {
pub async fn foo<M>(&self, message: &M) -> Result<(), Error>
where
M: Message + Serialize + Sync,
{
// ...
}
error[E0412]: cannot find type `M` in this scope
--> foo.rs:349:52
|
349 | pub async fn foo<M>(&self, message: &M) -> Result<(), Error>
| ^ not found in this scope
If possible, this should work by switching to using the impl _
syntax:
pub async fn foo<M>(&self, message: &impl Message + Serialize + Sync) -> Result<(), Error> {
// ...
}
That being said, I should look into this since it is not always possible to use the impl _
syntax so thanks for bringing this up! (:
Interesting side discovery: faux forces all function impl
traits to also implement Debug
. There's a {:?}
somewhere inside faux::when!
.
Trying to please faux with impl
leads to some furious build errors:
use std::fmt::Debug;
trait A: Debug {}
#[cfg_attr(test, faux::create)]
struct S {}
#[cfg_attr(test, faux::methods)]
impl S {
pub fn foo(&self, _a: &impl A) {
println!("foo")
}
}
fn main() {
#[derive(Debug)]
struct T(u8);
impl A for T {}
let t = T(42);
let s = S {};
s.foo(&t);
}
#[cfg(test)]
#[test]
fn repro() {
let mut mock = S::faux();
faux::when!(mock.foo).then(|_| println!("mock foo"));
}
$ cargo run
[...]
foo
$ cargo test
[...]
error[E0720]: cannot resolve opaque type
--> src/main.rs:9:28
|
7 | #[cfg_attr(test, faux::methods)]
| ------------- returning here with type `When<&S, &impl A, (), faux::when::Any>`
8 | impl S {
9 | pub fn foo(&self, _a: &impl A) {
| ^^^^^^ recursive opaque type
error[E0063]: missing field `0` in initializer of `S`
--> src/main.rs:19:13
|
19 | let s = S {};
| ^ missing `0`
Ah! 🤦🏾♂️
That's totally my bad. I just realized my impl Trait
solution does not work when doing &impl Trait
. In theory this should be an easy fix so I will look into it soon.
That being said, the trait does not need to implement debug at all. See the tests. Additionally, this part of the error:
error[E0063]: missing field `0` in initializer of `S`
--> src/main.rs:19:13
|
19 | let s = S {};
| ^ missing `0`
has to do with the your main function trying to create an S
without using a function that is wrapped by #[faux::methods]
but rather trying to create it directly. Mockable structs can only be created by using functions/methods that are wrapped inside the #[faux::methods]
attribute.
Ah yes, adding an S::new
that's inside #[faux::methods]
and using that gets rid of that error, just leaving the recursive opaque type one.
I have a fix up but I was having issues with releasing yesterday, my plan is to get a release for this today or tomorrow. You can see the updated tests and let me know if you think it isn't covering your use case: https://github.com/nrxus/faux/blob/master/tests/generic_methods.rs
@tv42 I just published a new release that should have this fix. Try it out and let me know! (:
Hi! Did you find any workaround or fix for output generics?
I have a function that's signature is:
pub async fn get_authenticated_request<T: DeserializeOwned>(
&self,
path: &str,
token: &str,
) -> Result<T, CommonError>
And found no workaround to make it build
@nrxus any ideas?
Hey @jmoguilevsky, things get hectic during the holidays so I wasn't looking at faux
at all 😅 .
Hm this use case isn't supported by faux
yet unfortunately although looking at my explanation from a year ago I think I could create a hack around to at least get it to compile, although maybe not make them mockable. That'll take me a little bit to implement though and my time has been scarce lately unfortunately.
For a workaround for now, if possible, I would try to have that method in an impl
block that is not wrapped by #[cfg_attr(test, faux::methods)]
. The tricky thing though is that if you do that you cannot use any of the fields in the struct directly but you would have to go through methods that are inside an impl
block with the #[cfg_attr(test, faux::methods)]
attribute. Something like:
#[cfg_attr(test, faux::create)]
struct Foo {
/* snip */
}
// THIS ONE IS WRAPPED BY `faux::methods`
#[cfg_attr(test, faux::methods)]
impl Foo {
async fn inner_authenticated_request(
&self,
path: &str,
token: &str,
) -> Result<String, CommonError> {
/* snip your implementation */
}
}
// NOT WRAPPED BY `faux::methods`
impl Foo {
pub async fn get_authenticated_request<T: DeserializeOwned>(
&self,
path: &str,
token: &str,
) -> Result<T, CommonError> {
let raw = self.inner_authenticated_request().await?;
/* snip convert raw string to deserialized T */
}
}