在Rust中测试的详细指南

162 阅读6分钟

一个程序员可以拥有的最伟大的工具之一是编写测试的能力,以确保他们所编写的代码功能良好。当然,Rust也有编写测试的方法,这使得测试驱动的开发周期得以实现。

目录

  1. 测试驱动的开发
  2. Rust中的简单测试
  3. 测试标志
  4. 例子

测试驱动的开发

这是什么意思?好吧,假设你想写一个两个数字相加的函数。你可以先写一个测试,其中你说,如果我用2和3作为两个参数调用这个函数,结果应该是5。然后,你写出这个函数,用这个测试来确定这个函数是否按计划工作。这样一来,在你开始写代码之前,你就已经设定了你的代码必须通过的小目标和检查

Rust有一些宏和属性来帮助我们写测试的追求。让我们来探索一些,好吗?

Rust中的简单测试

在Rust中进行测试的最简单方法是使用测试配置和测试宏。要运行如下配置的测试,你只需在终端运行一个good test命令即可:

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2+2, 4);
    }
}

Test-1

在函数前面的#[test]宏表示它是一个测试函数,所以测试运行器知道它必须运行这个函数作为测试的一部分。足够简单了!

好的,但是如果有什么东西失败了,会是什么样子呢?让我们强制一个总是失败的测试,让我们看看:

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }

    #[test]
    fn please_dont_panic() {
        panic!("Oops");
    }
}

Test-2

正如我们可以看到的,我们的测试失败了。因为我们的一个测试失败了,而且它告诉我们哪个特定的测试失败了。除了惊慌失措之外,还有其他一些使测试失败的方法。

其中一个方法是assert宏。Assert接受一个布尔参数。如果它是真的,assert不做任何事情。然而,如果它是假的,它就会惊慌失措!例如......让我们做一些非常愚蠢的事情,只是为了说明这一点!

is_this_4函数返回一个bool。如果是4,则为真,否则为假。所以我们可以用assert来处理这个问题!

#[cfg(test)]
mod tests {
    fn is_this_4(n: i32) -> bool {
        if n == 4 {
            return true;
        } else {
            return false;
        }
    }

    #[test]
    fn test_four() {
        assert!(is_this_4(4));
    }

    #[test]
    fn test_five() {
        assert!(is_this_4(5));
    }
}

Test-3

我们的用例,最好使用Rust提供的另外两个宏,assert_eq!和assert_ne!,它们分别检查相等和不相等。我们可以把上面的测试改写成...

    #[test]
    fn test_four() {
        assert_eq!(4, 4);
    }

    #[test]
    fn test_five() {
        assert_eq!(5, 4);
    }

我不打算贴出图片,因为它和上面的测试输出完全一样。

在assert!、assert_eq!和assert_ne!中,你还可以指定一些额外的信息,以便在测试失败时显示。像这样:

#[cfg(test)]
mod tests {
    #[test]
    fn test_five() {
        assert!(5 == 4, "5 is not equal to 4, clearly");
    }
}

Test-4

我们的另一个测试选项是#[should_panic]属性。任何被标记为该属性的函数,都会出现恐慌,如果它没有出现恐慌,测试就会失败:

#[cfg(test)]
mod tests {
    pub fn check_lower_100(n: i32) -> bool {
        if n > 100 {
            return false;
        } else {
            return true;
        }
    }

    #[test]
    #[should_panic]
    fn get_correct_value() {
        assert!(check_lower_100(75));
    }

    #[test]
    #[should_panic]
    fn get_high_value() {
        assert!(check_lower_100(150));
    }
}

Test-5

正如你所看到的,当标记为should panic时,通过的测试就会失败,而不通过的则会通过我们在 "捕捉 "或期待错误的发生。这一点我以前当然也用过。我知道某些值应该破坏一个函数,我就这样测试。

关于之前的文章,我们也可以有一个Result<T, E>作为返回类型!我借用Rust书中的例子(稍作改动),作为借口,也给大家指点一下下面的参考资料:

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() -> Result<(), String> {
        if 2 + 2 == 5 {
            Ok(())
        } else {
            Err(String::from("two plus two does not equal five"))
        }
    }
}

所以,如果函数成功了,我们就返回Ok(()),如果失败了,就会显示我们传递给Err的字符串!

Test-6

请记住,如果你使用Result<T, E>的返回类型,你就不能使用should_panic属性!

测试标志

我们可以在测试命令中添加一些额外的标志来控制这些测试的运行方式:

cargo test -- --test-threads=1 // Lets you set how many threads you want to use to run your tests, probably unnecessary unless you have a LOT of tests

cargo test -- --show-output // Tests don't print output to the terminal. By using this flag, you will get to see the output.

cargo test name // replace name with the function name of the test you want to execute, and only that one will be executed.

cargo test -- --ignored // Runs all the tests that are labeled with the ignore attribute

还有#[忽略]属性,你可以添加,就像should_panic属性一样,使特定的测试不会被运行,除非特别要求。

例子!

好的。让我们假设我们想做一个函数,给定一个字符串,如果它包含我们指定的另一个字符串,则返回真,如果不包含则返回假。很简单!

考虑到我们对字符串片断和寿命的了解,我们的函数签名可能看起来有点像这样:

fn search_for<'a>(contents: &'a str, query: &'a str) -> bool

所以,让我们写一个我们知道应该失败的测试,和一个我们知道应该通过的测试:

#[cfg(test)]
mod tests {
    use super::search_for;

    #[test]
    fn test_search_for_valid() {
        let content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. 
        Donec imperdiet ullamcorper eros eget dictum.
        Integer vel metus malesuada nisi elementum posuere.";
        
        let query = "Lorem ipsum";

        assert_eq!(search_for(content, query), true);
    }

    #[test]
    fn test_search_for_invalid() {
        let content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. 
        Donec imperdiet ullamcorper eros eget dictum.
        Integer vel metus malesuada nisi elementum posuere.";

        let query = "Rust was here";

        assert_eq!(search_for(content, query), false);
    }
}

所以,根据测试结果,当我们寻找内容中存在的东西时,我们的搜索函数应该返回true,而当它不存在时,则返回false。让我们在编写搜索函数时牢记这一点。我们逐行搜索我们的文本,如果该行包含我们的查询,我们就返回true。如果我们浏览了所有的行,还没有返回,我们就返回false,因为我们还没有找到查询:

pub fn search_for<'a>(contents: &'a str, query: &'a str) -> bool {
    for line in contents.lines() {
        if line.contains(query) {
            return true;
        }
    }
    false
}

所以,我们运行我们的测试。如果都通过了,我们就把这个函数写好了。如果我们有任何bug,它们就会失败。因为我们在写函数本身之前,就已经写好了测试,给了我们预期的结果。"这应该像这样工作",然后围绕这个想法写代码:

Test-7

今天就到此为止了。试着做一个小练习,首先想到一个结果,一个预期的功能,写测试以确保它发生,然后再写这个功能。比如......也许是计算一个数字的阶乘的东西。这很简单,你可以用测试驱动开发来试试。

参考资料

The Rust Book中的测试章节