-
-
Notifications
You must be signed in to change notification settings - Fork 193
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
Feature Request: Abstractable Types for unit testing #36
Comments
Thanks for starting an interesting design discussion! 🙂 #[test]
fn test_app_can_be_paused() {
let base = InMemoryBase::new();
{
let mut app_state = AppState::new(&mut base);
app_state.pause()
}
assert!(base.get_signals().contains("paused"));
} If I understand you correctly, you're asking for a mock for To understand your use case better, two questions:
|
Thanks Bromeon, My thinking here is that I want to test my code, not the libraries code, so being able to almost entirely abstract out the library from my code would be the ideal circumstance. I actually think I might be onto something that would let people write their own mocks simply by adding in generics and where clauses to godot-macros. From the code it looks like it was the plan to do this at some point. My changes to godot-macros builds at least, I'm just having a bit of trouble with the godot part of the project due to some issue with clang. 🤔 I'll open a PR for further discussion, one sec. 😄 |
The PR has a little more info. #37 For clarity, I just picked emit_signal for the example, I think you should be able to stub/mock whatever functionality you like. |
Simply making the
All these operations are not meaningfully possible with a "fake base" (aka mock), and most of them need to be available at compile time for type safety. Also, many types (even fundamental ones like That said, I totally understand your use case of abstracting away Godot interactions. I just think the abstraction boundary should be moved a bit 🙂 Concretely, due to the contract mentioned above, the most straightforward solution might be to not derive #[cfg_attr(not(test), derive(GodotClass, Debug))]
#[cfg_attr(not(test), class(base = Node))]
pub struct AppState {
// Running in engine
#[cfg(not(test))
#[base]
base: Base<Node>,
// Running in unit-test
#[cfg(test)]
base: NodeMock, // your custom impl
} Maybe some of these things could be made a bit more ergonomic over time, but it might already serve as a start. |
Oh nice! I tried something similar to that but couldn't get it working, this does... nearly 😂 It works for my tests but breaks when used with #[cfg_attr(not(test), godot_api)]
impl AppState {
//...
#[cfg_attr(not(test), func)]
pub fn is_not_playing(&self) -> bool {
self.state == State::NotPlaying
}
//...
} This works in test, but in build I get:
If I remove the
If I remove the
This is super close though, and I can live with my code being a hilarious proportion of annotations 😅 |
Solved... with even more annotations 😂 The problem is, Rust doesn't necessarily evaluate cfg or cfg_attr in a sensible way, so its turnning on In my crate root, I turn on the nightly feature for cfg_eval #![feature(cfg_eval)] Then I add cfg_eval to the struct impl: #[cfg_eval]
#[cfg_attr(not(test), godot_api)]
impl AppState {
// ...
#[cfg_attr(not(test), func)]
pub fn is_not_playing(&self) -> bool {
self.state == State::NotPlaying
}
} Now it evaluates all cfg annotations first, then deals with everything else. |
Thank you so much @Bromeon, happy to close this unless you particularly want to look at alternatives |
I see -- the problem is that One solution I can think of right now: you would write your own proc-macro like #[cfg_attr(test, my_test_api)] // <- your proc macro attribute
#[cfg_attr(not(test), godot_api)] // <- godot-rust's
impl AppState {
#[func] // <- always like this, no cfg
pub fn is_not_playing(&self) -> bool {
self.state == State::NotPlaying
}
} You can look into the You could even try that your proc-macro outputs a #[my_test_api] // <- in non-test, evaluates to: #[godot_api] impl AppState { /* verbatim */ }
impl AppState {
#[func]
pub fn is_not_playing(&self) -> bool {
self.state == State::NotPlaying
}
} Just saw you found in the meantime an approach with |
Closing -- the problem here has been addressed through conditional compilation, see last few posts 🙂 More utilities for testing/mocking might emerge in the future, but this issue is a bit too specific for a general approach to that. |
I'm going to try adding two new attributes A much nicer UX, I think, would be to have a |
One of the big reasons to choose Rust for game dev is its incredibly good built in testing tools.
Currently, the way that we wire custom classes, its not possible to write unit tests as the struct depends on the godot engine being present.
Take this overly simplified class:
You might want to check that the logic in pause works and that
emit_signal
was called withpaused
as a parameter.One way we could do this is by passing AppState a version of Base that stores what signals have been called:
I've tried a few of the suggestions for abstraction to allow my core logic to be tested but largely means writing a second struct with the same function names and then just having one call the other, meaning not only is it a lot of extra work to do this, but still leaves large parts of the code untestable, not to mention things like signals which can't be called from the other structs. For things like AppState it really doesn't make sense to have separate logic as all its doing is managing the changing of state and allowing other things to query the state so there's no way for me to test it.
I would love to help on this, but don't really know where to start in the codebase. It seems like we'd have to make a Trait around
godot::engine::Object
, then somehow allow Custom Class's to take other things thatimpl
that trait without breaking the custom macro's ability to wire the struct.I'm sure there are other ways to get the same results too.
Thoughts?
The text was updated successfully, but these errors were encountered: