Iterating on Testing in Rust
epage.github.io
epage.github.io
While I like Rust's `[#test]` can be placed anywhere and will be found/run by cargo test, I think there should also be something like `[#test_context]` where you receive a handle to the test framework's entry point (a little like Go) and from there you just write code to create tests... this is a stupid simple way to solve every problem mentioned in this post. Dart has something like this, and it's amazing for me to realize that its totally simplistic solution makes everything those complex Java testing frameworks do (test by example cases, skip depending on a function's return value, group tests into sub-tests etc.) not just possible, but easier and more fun to use as it's just "normal" code (your IDE can autocomplete your stuff so you don't need to google every tinme for the magic combination of annotations/parameters/types to use).
This is what it may look like in Rust:
#[test_context]
fn make_tests(t: &mut TestHarness) {
t.newGroup("group 1", |t| {
t.newTest("test something", |t| {
assert_eq(...);
});
for (input, expected) in &[("a", "a1"), ("b", "b1")] {
t.newTest(format!("my_fun({}) == {}", input, expected), || {
assert_eq(my_fun(input), expected);
}),
}
});
}
Please stop using Java annotation-like stuff for this kind of thing, it just limits what you can do in exchange for looking a little prettier (but being vastly more complex when the whole implementation is taken into consideration).Here is how I made dynamic tests for it.
But cargo nextest is a game changer. Colored output + fast fail (configurable) + timeout detection is just great.
Do you want the dynamically generated tests reported in discover? If so, then you need to assume that if you pass in the reference that it will only be used for dynamically generating tests and not for helping with skipping of tests or other use cases.
Personally, I feel like this does not fully cover the fixture use case.
That's one way in which this can be interpreted, but I don't really agree. I think the problems are a classic case of a "good enough" solution that shipped very early and became core of the ecosystem. Now it's hard to move off it or to improve it as it's in a case of stasis.
Even today you do not need to use `#[test]` or the built-in functionality at all. (The exception being that the internals of the print interception are not exposed, but there are other ways around that).
Fundamentally the frustration of the state is not high enough, and nobody pushes strongly for a much improved testing solution. But technology wise, you would not need anything from the language to have a better test system in Rust.
BTW, that's the exact reason I like lack of a “good standard library” in JS that people usually cite as a downside. When there's a good enough default solution it suffocates evolution of different ideas in the field.
> Data driven tests are an easy way to cover a lot of cases (granted, property testing is even better). The most trivial way of doing this is just looping over your cases
> […]
> You don't know which input was being processed on failure (without extra steps)
> Any debug output from prior iterations will flood the display when analyzing a failure
For generating separate tests for different inputs, keeping it easy to see which input failed the test I use the test-case crate
https://crates.io/crates/test-case
Here’s an example from the test-case readme:
#[cfg(test)]
mod tests {
use test_case::test_case;
#[test_case(-2, -4 ; "when both operands are negative")]
#[test_case(2, 4 ; "when both operands are positive")]
#[test_case(4, 2 ; "when operands are swapped")]
fn multiplication_tests(x: i8, y: i8) {
let actual = (x * y).abs();
assert_eq!(8, actual)
}
}
And then when you run cargo test
You get this output: running 3 tests
test tests::multiplication_tests::when_both_operands_are_negative ... ok
test tests::multiplication_tests::when_both_operands_are_positive ... ok
test tests::multiplication_tests::when_operands_are_swapped ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sBut at compile time without static analysis tools, this is so cool.
@pytest.mark.parametrize
https://docs.pytest.org/en/7.2.x/how-to/parametrize.html#pyt...
#[test_case(-2, -4, 8 ; "when both operands are negative")]
#[test_case(2, 4, 8 ; "when both operands are positive")]
#[test_case(4, -2, 8 ; "when one operands is negative")]
fn multiplication_tests(x: i8, y: i8) -> i8 {
x * y
}How do you ensure this actually works and for example emailing isn't broken accidentaly by a future refactor?
Of course that you can and should test each individual component in this scenario, but at some point you need to mock more than "the thing that does requests" and check if it click with other components.
trait TwilioClient {
send_sms(num: &str, text: &str)
}
struct TwilioClientHttp {
client: HttpClient
}
impl TwilioClient for TwilioClientHttp {...}
struct TwilioClientFake {}
impl TwilioClient for TwilioClientFake {...}
And that's nice because now we have that abstraction layer away from IO. This will feel like overkill in some cases for sure, but I do find it great for a lot of cases. You can even just make that TwilioClientFake inside of the testing area itself directly. Go has done a good job pushing the idea of accepting interfaces everywhere, but it's ad hoc construction of those is a bit leaner.Sorry for the StackOverflow-esque non-answer though. Also excuse my Rust, it's a bit rusty.
You can replicate it partially with a OnceCell, a function call in each test and RAII but not ideal.
pub async fn test<T, U>(test: T)
where
T: FnOnce() -> U,
U: Future,
{
setup().await;
let result = async move { panic::AssertUnwindSafe(test()).catch_unwind().await }.await;
teardown().await;
assert!(result.is_ok())
}