Skip to content

Commit

Permalink
Merge pull request #275 from adwhit/alex/less-test-boilerplate
Browse files Browse the repository at this point in the history
Add helper methods to `Update` to remove test boilerplate
  • Loading branch information
StuartHarris authored Oct 21, 2024
2 parents 01f6e25 + 0c028eb commit 4e82333
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 98 deletions.
56 changes: 56 additions & 0 deletions crux_core/src/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ where
Ok(self.context.updates())
}

/// Resolve an effect `request` from previous update, then run the resulting event
///
/// This helper is useful for the common case where one expects the effect to resolve
/// to exactly one event, which should then be run by the app.
pub fn resolve_to_event_then_update<Op: Operation>(
&self,
request: &mut Request<Op>,
value: Op::Output,
model: &mut App::Model,
) -> Update<Ef, App::Event> {
request.resolve(value).expect("failed to resolve request");
let event = self.context.updates().expect_one_event();
self.update(event, model)
}

/// Run the app's `view` function with a model state
pub fn view(&self, model: &App::Model) -> App::ViewModel {
self.app.view(model)
Expand Down Expand Up @@ -128,6 +143,7 @@ impl<Ef, Ev> AppContext<Ef, Ev> {
/// Update test helper holds the result of running an app update using [`AppTester::update`]
/// or resolving a request with [`AppTester::resolve`].
#[derive(Debug)]
#[must_use]
pub struct Update<Ef, Ev> {
/// Effects requested from the update run
pub effects: Vec<Ef>,
Expand All @@ -147,6 +163,46 @@ impl<Ef, Ev> Update<Ef, Ev> {
pub fn effects_mut(&mut self) -> impl Iterator<Item = &mut Ef> {
self.effects.iter_mut()
}

/// Assert that the update contains exactly one effect and zero events,
/// and return the effect
pub fn expect_one_effect(mut self) -> Ef {
if self.events.is_empty() && self.effects.len() == 1 {
self.effects.pop().unwrap()
} else {
panic!(
"Expected one effect but found {} effect(s) and {} event(s)",
self.effects.len(),
self.events.len()
);
}
}

/// Assert that the update contains exactly one event and zero effects,
/// and return the event
pub fn expect_one_event(mut self) -> Ev {
if self.effects.is_empty() && self.events.len() == 1 {
self.events.pop().unwrap()
} else {
panic!(
"Expected one event but found {} effect(s) and {} event(s)",
self.effects.len(),
self.events.len()
);
}
}

/// Assert that the update contains no effects or events
pub fn assert_empty(self) {
if self.effects.is_empty() && self.events.is_empty() {
return;
}
panic!(
"Expected empty update but found {} effect(s) and {} event(s)",
self.effects.len(),
self.events.len()
);
}
}

/// Panics if the pattern doesn't match an `Effect` from the specified `Update`
Expand Down
2 changes: 1 addition & 1 deletion crux_http/tests/with_tester.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ mod tests {
});

for event in update.events {
app.update(event, &mut model);
app.update(event, &mut model).assert_empty();
}
assert_eq!(model.body, "\"hello\"");
assert_eq!(model.values, vec!["my_value1", "my_value2"]);
Expand Down
96 changes: 39 additions & 57 deletions crux_kv/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ fn test_get() {
.unwrap();

let event = updated.events.into_iter().next().unwrap();
app.update(event, &mut model);
let _update = app.update(event, &mut model);

assert_eq!(model.value, 42);
}
Expand Down Expand Up @@ -198,7 +198,7 @@ fn test_set() {
.unwrap();

let event = updated.events.into_iter().next().unwrap();
app.update(event, &mut model);
let _update = app.update(event, &mut model);

assert!(model.successful);
}
Expand All @@ -208,32 +208,26 @@ fn test_delete() {
let app = AppTester::<App, _>::default();
let mut model = Model::default();

let updated = app.update(Event::Delete, &mut model);

let effect = updated.into_effects().next().unwrap();
let Effect::KeyValue(mut request) = effect else {
panic!("Expected KeyValue effect");
};
let mut request = app
.update(Event::Delete, &mut model)
.expect_one_effect()
.expect_key_value();

let KeyValueOperation::Delete { key } = request.operation.clone() else {
panic!("Expected delete operation");
};

assert_eq!(key, "test");

let updated = app
.resolve(
&mut request,
KeyValueResult::Ok {
response: KeyValueResponse::Delete {
previous: Value::None,
},
let _updated = app.resolve_to_event_then_update(
&mut request,
KeyValueResult::Ok {
response: KeyValueResponse::Delete {
previous: Value::None,
},
)
.unwrap();

let event = updated.events.into_iter().next().unwrap();
app.update(event, &mut model);
},
&mut model,
);

assert!(model.successful);
}
Expand All @@ -243,30 +237,24 @@ fn test_exists() {
let app = AppTester::<App, _>::default();
let mut model = Model::default();

let updated = app.update(Event::Exists, &mut model);

let effect = updated.into_effects().next().unwrap();
let Effect::KeyValue(mut request) = effect else {
panic!("Expected KeyValue effect");
};
let mut request = app
.update(Event::Exists, &mut model)
.expect_one_effect()
.expect_key_value();

let KeyValueOperation::Exists { key } = request.operation.clone() else {
panic!("Expected exists operation");
};

assert_eq!(key, "test");

let updated = app
.resolve(
&mut request,
KeyValueResult::Ok {
response: KeyValueResponse::Exists { is_present: true },
},
)
.unwrap();

let event = updated.events.into_iter().next().unwrap();
app.update(event, &mut model);
let _updated = app.resolve_to_event_then_update(
&mut request,
KeyValueResult::Ok {
response: KeyValueResponse::Exists { is_present: true },
},
&mut model,
);

assert!(model.successful);
}
Expand All @@ -276,12 +264,10 @@ fn test_list_keys() {
let app = AppTester::<App, _>::default();
let mut model = Model::default();

let updated = app.update(Event::ListKeys, &mut model);

let effect = updated.into_effects().next().unwrap();
let Effect::KeyValue(mut request) = effect else {
panic!("Expected KeyValue effect");
};
let mut request = app
.update(Event::ListKeys, &mut model)
.expect_one_effect()
.expect_key_value();

let KeyValueOperation::ListKeys { prefix, cursor } = request.operation.clone() else {
panic!("Expected list keys operation");
Expand All @@ -290,20 +276,16 @@ fn test_list_keys() {
assert_eq!(prefix, "test:");
assert_eq!(cursor, 0);

let updated = app
.resolve(
&mut request,
KeyValueResult::Ok {
response: KeyValueResponse::ListKeys {
keys: vec!["test:1".to_string(), "test:2".to_string()],
next_cursor: 2,
},
let _update = app.resolve_to_event_then_update(
&mut request,
KeyValueResult::Ok {
response: KeyValueResponse::ListKeys {
keys: vec!["test:1".to_string(), "test:2".to_string()],
next_cursor: 2,
},
)
.unwrap();

let event = updated.events.into_iter().next().unwrap();
app.update(event, &mut model);
},
&mut model,
);

assert_eq!(model.keys, vec!["test:1".to_string(), "test:2".to_string()]);
assert_eq!(model.cursor, 2);
Expand Down Expand Up @@ -362,7 +344,7 @@ pub fn test_kv_async() -> Result<()> {
.unwrap();

let event = update.events.into_iter().next().unwrap();
app.update(event, &mut model);
let _update = app.update(event, &mut model);

assert!(model.successful);

Expand Down
88 changes: 88 additions & 0 deletions crux_macros/src/effect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ impl ToTokens for EffectStructReceiver {

let filter_fn = format_ident!("is_{}", field_name);
let map_fn = format_ident!("into_{}", field_name);
let expect_fn = format_ident!("expect_{}", field_name);
let name_as_str = field_name.to_string();
filters.push(quote! {
impl #effect_name {
pub fn #filter_fn(&self) -> bool {
Expand All @@ -130,6 +132,13 @@ impl ToTokens for EffectStructReceiver {
None
}
}
pub fn #expect_fn(self) -> crux_core::Request<<#capability<#event> as ::crux_core::capability::Capability<#event>>::Operation> {
if let #effect_name::#variant(request) = self {
request
} else {
panic!("not a {} effect", #name_as_str)
}
}
}
});
}
Expand Down Expand Up @@ -275,6 +284,17 @@ mod tests {
> {
if let Effect::Render(request) = self { Some(request) } else { None }
}
pub fn expect_render(
self,
) -> crux_core::Request<
<Render<Event> as ::crux_core::capability::Capability<Event>>::Operation,
> {
if let Effect::Render(request) = self {
request
} else {
panic!("not a {} effect", "render")
}
}
}
"###);
}
Expand Down Expand Up @@ -346,6 +366,17 @@ mod tests {
> {
if let Effect::Render(request) = self { Some(request) } else { None }
}
pub fn expect_render(
self,
) -> crux_core::Request<
<Render<Event> as ::crux_core::capability::Capability<Event>>::Operation,
> {
if let Effect::Render(request) = self {
request
} else {
panic!("not a {} effect", "render")
}
}
}
"###);
}
Expand Down Expand Up @@ -460,6 +491,19 @@ mod tests {
> {
if let MyEffect::Http(request) = self { Some(request) } else { None }
}
pub fn expect_http(
self,
) -> crux_core::Request<
<crux_http::Http<
MyEvent,
> as ::crux_core::capability::Capability<MyEvent>>::Operation,
> {
if let MyEffect::Http(request) = self {
request
} else {
panic!("not a {} effect", "http")
}
}
}
impl MyEffect {
pub fn is_key_value(&self) -> bool {
Expand All @@ -476,6 +520,17 @@ mod tests {
> {
if let MyEffect::KeyValue(request) = self { Some(request) } else { None }
}
pub fn expect_key_value(
self,
) -> crux_core::Request<
<KeyValue<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
> {
if let MyEffect::KeyValue(request) = self {
request
} else {
panic!("not a {} effect", "key_value")
}
}
}
impl MyEffect {
pub fn is_platform(&self) -> bool {
Expand All @@ -492,6 +547,17 @@ mod tests {
> {
if let MyEffect::Platform(request) = self { Some(request) } else { None }
}
pub fn expect_platform(
self,
) -> crux_core::Request<
<Platform<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
> {
if let MyEffect::Platform(request) = self {
request
} else {
panic!("not a {} effect", "platform")
}
}
}
impl MyEffect {
pub fn is_render(&self) -> bool {
Expand All @@ -506,6 +572,17 @@ mod tests {
> {
if let MyEffect::Render(request) = self { Some(request) } else { None }
}
pub fn expect_render(
self,
) -> crux_core::Request<
<Render<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
> {
if let MyEffect::Render(request) = self {
request
} else {
panic!("not a {} effect", "render")
}
}
}
impl MyEffect {
pub fn is_time(&self) -> bool {
Expand All @@ -520,6 +597,17 @@ mod tests {
> {
if let MyEffect::Time(request) = self { Some(request) } else { None }
}
pub fn expect_time(
self,
) -> crux_core::Request<
<Time<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
> {
if let MyEffect::Time(request) = self {
request
} else {
panic!("not a {} effect", "time")
}
}
}
"###);
}
Expand Down
Loading

0 comments on commit 4e82333

Please sign in to comment.