Proper way to test functions that do the same thing

I have two implementations of a function that do the same thing, which in my case is to reverse words in a string. I want to be able to cleanly test both functions using the same inputs and expected outputs, and the following is the best I could come up with:

let fn_arr: Vec<fn(&str) -> String> = vec![reverse_words, reverse_words2::reverse_words];

for f in fn_arr.iter() {
    assert_eq!(f("apple"), "elppa");
    assert_eq!(
        f("The quick brown fox jumps over the lazy dog."),
        "ehT kciuq nworb xof spmuj revo eht yzal .god"
    );
    assert_eq!(f("a b c d"), "a b c d");
    assert_eq!(f("double  spaced  words"), "elbuod  decaps  sdrow");
}

Is there any better way to do this? Are there built-in ways to make this cleaner using #[cfg(test)]? I get that this might be a trivial thing to do, and in general, we should avoid functions that do the same thing, but I just want to make sure I am making the most of what Rust can do. Is there a general way to say that two functions are "equal" or equivalent?


Other details:

The first function's implementation in src/main.rs:

fn reverse_words(str: &str) -> String {
    let mut total_collector = Vec::new();
    let mut mini_collector = Vec::new();
    for (i, c) in str.chars().enumerate() {
        if c.is_whitespace() {
            if !mini_collector.is_empty() {
                mini_collector.reverse();
                total_collector.append(&mut mini_collector);
                mini_collector.truncate(0);
            }
            total_collector.push(c);
        } else {
            mini_collector.push(c);
        }

        if i == str.len() - 1 {
            mini_collector.reverse();
            total_collector.append(&mut mini_collector);
            mini_collector.truncate(0)
        }
    }
    total_collector.into_iter().collect::<String>()
}

The second function's implementation in src/reverse_words2/mod.rs:

pub fn reverse_words(str: &str) -> String {
    /*

    The following one-liner in the function only works because of
    the following trick:

    ```Rust
    let x = "    a  b c".to_string();
    let d: Vec<_> = x.split(' ').collect();
    assert_eq!(d, &["", "", "", "", "a", "", "b", "c"]);
    ```

    Note that the string has 4 spaces before a, 2 spaces before b,
    and 1 space before c. However, when split, the resulting vector
    has 4 spaces, then a, then 1 space, then b, then 0 spaces, then c.

    Contiguous separators can lead to possibly surprising behavior
    when whitespace is used as the separator. This trick allows this
    to work on sentences with an arbitrary number of spaces in between
    words.

    */

    str.split(' ')
        .map(|word| word.chars().rev().collect::<String>())
        .collect::<Vec<String>>()
        .join(" ")
}

I'm adding these because I'd also take any feedback on if these functions should be organized in different files.

Personally I would probably use a macro for this.

macro_rules! assert_reverse_eq {
    ($left:expr, $right:expr) => {
        assert_eq!($crate::reverse_words($left), $right);
        assert_eq!($crate::reverse_words2::reverse_words($left), $right);
    };
}

#[test]
fn both() {
    assert_reverse_eq!("apple", "elppa");
    assert_reverse_eq!(
        "The quick brown fox jumps over the lazy dog.",
        "ehT kciuq nworb xof spmuj revo eht yzal .god"
    );
    assert_reverse_eq!("a b c d", "a b c d");
    assert_reverse_eq!("double  spaced  words", "elbuod  decaps  sdrow");
}

There's nothing wrong with doing it the way you're doing it though, and I imagine some people would prefer it over a macro.

2 Likes

I suggest using QuickCheck. As I understand it, you want to test the property that the functions have the same outputs for the same inputs. QuickCheck will try to find inputs for which this property is false.

5 Likes

Thank you! Always looking for cool new Rust things to try out. Will definitely try employing this.

This looks neat. I'm going to try using this; this makes more sense than just testing over a ton of inputs.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.