如何将 Rust 的 Cargo 中二进制文件的测试移动到单独的文件中?

54

我使用Cargo创建了一个新的二进制文件:

cargo new my_binary --bin

my_binary/src/main.rs 中的一个函数可用于测试:

fn function_from_main() {
    println!("Test OK");
}

#[test]
fn my_test() {
    function_from_main();
}

cargo test -- --nocapture 命令按预期运行测试。

最简单的方法是将这个测试移到一个单独的文件中(保持my_binary/src/main.rs中的function_from_main)?

我试过了,但不知道如何使my_test从一个单独的文件调用function_from_main

5个回答

79

Rust编程语言一个专门介绍测试的章节,建议阅读以了解基本概念。


通常将单元测试(可以更多地访问代码内部的测试)放置在每个特定文件中的test模块中:

fn function_from_main() {
    println!("Test OK");
}

#[cfg(test)]
mod test {
    use super::*;
    
    #[test]
    fn my_test() {
        function_from_main();
    }
}

模块可以移动到新文件中,尽管对于单元测试模块来说这是不常见的:

main.rs

fn function_from_main() {
    println!("Test OK");
}

#[cfg(test)]
mod test;

test.rs

use super::*;

#[test]
fn my_test() {
    function_from_main();
}

详细了解文件和模块如何相互映射,请参见将模块分离到不同文件中


将测试放在单独的文件中的更常见的情况是集成测试。这些也在书中被提到,有一个专门针对 crate 外部的测试的章节。这种类型的测试非常适合作为代码的使用者来执行。

文档的这一部分包括一个简介示例和描述性的文字:

我们在项目目录的顶层旁边创建一个名为tests的目录,与src相邻。Cargo 知道在此目录中查找集成测试文件。然后,我们可以在此目录中创建任意数量的测试文件,Cargo会将每个文件都编译为一个独立的 crate。

让我们创建一个集成测试。在代码清单 11-12 中仍然保留 src/lib.rs 文件中的代码,创建一个tests 目录,并创建一个名为 tests/integration_test.rs 的新文件,然后输入清单 11-13 中的代码:

文件名: tests/integration_test.rs

use adder;

#[test]
fn it_adds_two() {
    assert_eq!(4, adder::add_two(2));
}

清单11-13:对adder crate的一个函数进行集成测试

我们在代码顶部添加了use adder,而这在单元测试中是不需要的。原因是tests目录中的每个测试都是一个独立的crate,因此我们需要将库引入每个测试crate的作用域。

请注意,该函数被称为adder::add_two。有关Rust模块系统的更多详细信息可以在“包、Crate和模块”章节中找到。

由于这些测试会像用户一样使用您的crate,如果您想测试一个二进制文件,则应该执行该二进制文件。像assert_cmd这样的Crate可以帮助减轻这种类型测试的痛苦。

在其他情况下,您应该将大型二进制文件分解为大型库和小型二进制文件。然后,您可以为库的公共API编写集成测试。

另请参见:


5
如何为可执行文件编写集成测试(即tests目录中的测试)?由于extern crate <executable>不起作用,因此需要采取其他措施。 - Nawaz
4
如果你正在对一个可执行文件进行集成测试(integration tests),那么你需要像用户一样驱动它:通过使用类似于 std::process::Command 的命令来运行构建出来的命令。否则,你所说的就是单元测试(unit tests)。 - Shepmaster
使用调试器来集成测试通过生成一个单独的进程来测试二进制文件是一种次优的开发者体验。 :-( - Squirrel
@Shepmaster,我无法理解您的答案与@mmai答案之间的区别。您能否解释一下为什么将测试文件命名为test.rs可以工作,而将其命名为foo_test.rs在没有路径的情况下不起作用? - Tamil Vendhan Kanagarasu
@TamilVendhanKanagarasu,因为语言要求模块的文件名与模块的名称匹配。当你违反这个约定时,编译器怎么知道要查找哪个文件呢?我通常不鼓励人们使用#[path]属性,除非在非常特殊的情况下(而这不是其中之一)。 - Shepmaster

24
如果您有一个名为 foo.rs 的模块,并且想要将单元测试放在名为 foo_test.rs 的文件中,您会发现这并不总是Rust查找子模块的位置。
您可以使用#[path] 属性来指定与模块对应的文件的位置:
#[cfg(test)]
#[path = "./foo_test.rs"]
mod foo_test;

这在博客文章Better location for unit tests in Rust中有解释。


10
你为什么要费心将文件放在不符合惯例的位置呢?就像其他模块一样,把它命名为 foo.rsfoo/tests.rs - Shepmaster
4
这里没有任何激怒,因为我不需要修复你的代码!我只是觉得,如果我遵循语言的习惯用法而不是试图带着之前每种语言的所有负担前进,那么实现起来就会更加顺畅。 - Shepmaster
为什么需要#[path]?大多数人会选择使用mod foo_test,因为它看起来与lib.rs中的内容相同,但是他们会收到“文件未找到错误”。这是因为当您使用mod foo时,foo.rs中的所有代码现在都在模块foo的范围内 - 这会影响mod搜索的位置。因此,在模块foo的范围内,mod foo_test将查找src/foo/mod_test.rssrc/foo/mod_test/mod.rs。有点难以理解。此外,在foo_test.rs中,您需要编写use super::my_func来访问foo.rs(即foo模块)的成员。 - vaughan
@Shepmaster,我认为这是一个非常酷的可能性,可以让人们将单元测试与源代码分开,以防止污染源代码。实际上,这是我在阅读 Rust 书籍后搜索到的第一个问题,如果没有这个可能性,我可能会对 Rust 失去信心,很难相信将单元测试和源代码混合在一起是更可维护的方式。 - lnshi
从 Rust 书中:“惯例是在每个文件中创建一个名为 tests 的模块来包含测试函数,并使用 cfg(test) 注释该模块。”我会简单地遵循这个“惯例”。非习惯用法会让其他人更难理解你的代码。 - Aaron
2
虽然 Rust 在测试中包含了很多功能,但它并没有遵循所有其他测试框架的惯例。将代码分离到不同的文件中以帮助组织是一个好习惯;这同样适用于测试。个人而言,我希望能够通过翻页来查看文件中的关键点,我不想阅读与帮助我理解模块运行无关的行——特别是当我的测试占了一半以上的行数时。对我来说,这值得打破惯用的 Rust 使用方式;你可能会有不同的看法。 - Codebling

3

你是正确的;function_from_mainmain.rs 之外不可访问。

你需要创建一个 src/lib.rs 并将你想要逐个测试的函数移动到该文件中。然后你就可以在测试模块中使用 extern crate my_binary; 并且你的函数会出现在 my_binary 命名空间下。


2

我相信你应该遵循阅读Rust书籍测试章节的建议,但这仍然不能完全回答你的问题,即如何分离测试和源文件。

假设你有一个lib.rs源文件,并想要一个test_lib.rs文件。你所需要做的就是:

在你的lib.rs中:

mod test_lib;

// rest of source

然后在您的test_lib.rs文件中:
#[cfg(test)]
use super::*;

#[test]
fn test1() {
    // test logic 
}

// more tests

那么,你和都应该很高兴。

0
为了补充 @mmai 的回答:
#[rustfmt::skip]
#[cfg(test)]
#[path = "./foo_test.rs"]
mod foo_test;

cargo fmt 似乎对新测试文件的路径不满意。如果有更好的解决方案,我很乐意听取,因为我希望 cargo fmt 也能格式化我的测试文件!

*抱歉:我想将此作为现有答案的评论添加,但我没有足够的声望。


我不确定这个答案对mmai的回答有什么补充。此外,请不要在答案中提出跟进问题。那是一种讨论论坛风格,不适合本站的问答格式。 - cigien
由于目前所写的答案不够清晰明了,请进行编辑以添加更多细节,以帮助他人理解它是如何回答问题的。您可以在帮助中心找到有关如何编写良好答案的更多信息。 - Community
这并没有回答问题。一旦您有足够的声望,您就能对任何帖子发表评论;相反,提供不需要询问者澄清的答案。 - 来自审核 - Akida

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接