当前位置: 首页 > news >正文

定制网站建设广告西安网站建设成功建设

定制网站建设广告,西安网站建设成功建设,龙岗 网站建设,wordpress禁用谷歌的插件上一篇#xff1a; 10-通用类型、特质和生命周期 Edsger W. Dijkstra 在 1972 年发表的文章《The Humble Programmer》中说#xff1a;程序测试可以非常有效地显示错误的存在#xff0c;但对于显示错误的不存在却无能为力。这并不意味着我们不应该尽可能多地进行测试 10-通用类型、特质和生命周期 Edsger W. Dijkstra 在 1972 年发表的文章《The Humble Programmer》中说程序测试可以非常有效地显示错误的存在但对于显示错误的不存在却无能为力。这并不意味着我们不应该尽可能多地进行测试 程序的正确性是指我们的代码在多大程度上实现了我们的意图。Rust 的设计高度关注程序的正确性但正确性是复杂的而且不容易证明。Rust 的类型系统承担了这一重任的很大一部分但类型系统并不能囊括一切。因此Rust 支持编写自动化软件测试。 假设我们编写了一个函数 add_two 它可以在传给它的任何数字上加上 2。这个函数的签名接受一个整数作为参数并返回一个整数作为结果。当我们实现并编译该函数时Rust 会进行所有类型检查和借用检查例如确保我们没有向该函数传递 String 值或无效引用。但 Rust 无法检查这个函数是否会按照我们的意图运行即返回参数加 2而不是参数加 10 或参数减 50这就是测试的作用所在。 例如我们可以编写测试断言当我们将 3 传递给 add_two 函数时返回值是 5 。每当我们修改代码时都可以运行这些测试以确保现有的正确行为没有改变。 测试是一项复杂的技能虽然我们无法在一章中涵盖如何写好测试的所有细节但我们将讨论 Rust 测试设施的机制。我们将讨论编写测试时可用的注解和宏运行测试时提供的默认行为和选项以及如何将测试组织成单元测试和集成测试。 11.1 如何编写测试 测试是 Rust 函数用于验证非测试代码是否按预期方式运行。测试函数的主体通常执行以下三种操作 ①. 设置所需的数据或状态。 ②. 运行要测试的代码。 ③. 断言结果符合你的预期。 让我们看看 Rust 专门为编写执行这些操作的测试而提供的功能其中包括 test 属性、几个宏和 should_panic 属性。 11.1.1 测试功能剖析 最简单来说Rust 中的测试是一个带有 test 属性注释的函数。属性是 Rust 代码片段的元数据第 5 章结构体中使用的 derive 属性就是一个例子。要将函数改为测试函数请在 fn 之前一行添加 #[test] 。当你使用 cargo test 命令运行测试时Rust 会构建一个测试运行程序二进制文件运行注释函数并报告每个测试函数是否通过。 每当我们使用 Cargo 创建一个新的库项目时系统就会自动为我们生成一个包含测试函数的测试模块。这个模块为你提供了一个编写测试的模板这样你就不必在每次启动新项目时都去查找确切的结构和语法。您可以根据需要添加更多的测试函数和测试模块 在实际测试任何代码之前我们将通过试用模板测试来探索测试工作的某些方面。然后我们将编写一些实际测试调用我们编写的代码并断言其行为是正确的。 让我们创建一个名为 adder 的新库项目它将实现两个数字的相加 $ cargo new adder --libCreated library adder project $ cd adderadder 库中的 src/lib.rs 文件内容应与清单 11-1 一致。 #[cfg(test)] mod tests {use super::*;#[test]fn it_works() {let result add(2, 2);assert_eq!(result, 4);} }(清单 11-1cargo new自动生成的测试模块和函数 ) 现在让我们忽略最上面的两行把注意力集中在函数上。请注意 #[test] 注释该属性表示这是一个测试函数因此测试运行程序知道要将该函数视为测试。我们还可能在 tests 模块中使用非测试函数来帮助设置常用场景或执行常用操作因此我们始终需要指明哪些函数是测试函数。 示例函数体使用 assert_eq! 宏断言 result 包含 2 和 2 相加的结果等于 4。这个断言是一个典型测试格式的示例。让我们运行它看看测试是否通过。 cargo test 命令将运行项目中的所有测试如清单 11-2 所示。 cargo.exe testCompiling adder v0.1.0 (E:\rustProj\adder)Finished test [unoptimized debuginfo] target(s) in 1.25sRunning unittests src\lib.rs (target\debug\deps\adder-033e40fcb4baf750.exe)running 1 test test tests::it_works ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests adderrunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s (清单 11-2运行自动生成的测试的输出结果) 编译并运行测试。我们看到 running 1 test 这一行。下一行显示的是生成的测试函数名称名为 it_works 运行该测试的结果是 ok 。总体摘要 test result: ok. 表示所有测试都通过了读取 1 passed; 0 failed 的部分则是通过或未通过测试的总数。 我们可以将测试标记为忽略这样它就不会在特定实例中运行我们将在本章后面的 除非特别要求否则忽略某些测试 一节中介绍这一点。因为我们在这里没有这样做所以摘要显示 0 ignored 。我们还可以给 cargo test 命令传递一个参数只运行名称与字符串匹配的测试这叫做过滤我们将在 按名称运行测试子集 一节中介绍。我们还没有过滤正在运行的测试因此摘要末尾显示 0 filtered out 。 0 measured 统计量用于衡量性能的基准测试。截至本文撰写时基准测试仅在夜间 Rust 中提供。如需了解更多信息请参阅有关基准测试的文档。 从 Doc-tests adder 开始的测试输出的下一部分是任何文档测试的结果。我们还没有任何文档测试但 Rust 可以编译 API 文档中出现的任何代码示例。该功能有助于保持文档和代码的同步我们将在第 14 章 作为测试的文档注释 部分讨论如何编写文档测试。现在我们将忽略 Doc-tests 输出。 让我们开始根据自己的需要定制测试。首先将 it_works 函数的名称改为不同的名称例如 exploration 就像这样 #[cfg(test)] mod tests {#[test]fn exploration() {assert_eq!(2 2, 4);} } 然后再次运行 cargo test 。现在输出显示的是 exploration 而不是 it_works PS E:\rustProj\adder cargo.exe testrunning 1 test test tests::exploration ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests adderrunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 现在我们将添加另一个测试但这次我们要做一个失败的测试当测试函数中的某个函数崩溃时测试就会失败。每个测试都在一个新的线程中运行当主线程看到一个测试线程死亡时该测试就会被标记为失败。在第 9 章中我们谈到了最简单的 panic 方法就是调用 panic! 宏。将新测试作为名为 another 的函数输入这样你的 src/lib.rs 文件看起来就像清单 11-3。 #[cfg(test)] mod tests {#[test]fn exploration() {assert_eq!(2 2, 4);}#[test]fn another() {panic!(Make this test fail);} } (清单 11-3添加第二个会失败的测试因为我们调用了 panic! 宏) 使用 cargo test 再次运行测试。输出结果应与清单 11-4 类似其中显示 exploration 测试通过 another 测试失败。 cargo.exe testCompiling adder v0.1.0 (E:\rustProj\adder)Finished test [unoptimized debuginfo] target(s) in 0.38sRunning unittests src\lib.rs (target\debug\deps\adder-033e40fcb4baf750.exe)running 2 tests test tests::another ... FAILED test tests::exploration ... okfailures:---- tests::another stdout ---- thread tests::another panicked at src\lib.rs:10:9: Make this test fail note: run with RUST_BACKTRACE1 environment variable to display a backtracefailures:tests::anothertest result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: test failed, to rerun pass --lib (清单 11-4一项测试通过和一项测试失败时的测试结果) test tests::another 行显示的是 FAILED 而不是 ok 。在单个结果和摘要之间出现了两个新的部分第一个部分显示每个测试失败的详细原因。在本例中我们得到了 another 失败的详细信息因为它在 src/lib.rs 文件的第 10 行 panicked at Make this test fail 。下一部分只列出了所有失败测试的名称这在有大量测试和大量详细失败测试输出时非常有用。我们可以使用失败测试的名称来运行该测试以更方便地进行调试我们将在 控制测试运行方式 部分详细讨论运行测试的方法。 最后显示摘要行总的来说我们的测试结果是 FAILED 。其中一次测试通过一次测试失败。 现在你已经看到了不同场景下的测试结果让我们来看看 panic! 以外的一些在测试中有用的宏。 11.1.2 使用 assert! 宏检查结果 当您要确保测试中的某些条件求值为 true 时标准库提供的 assert! 宏非常有用。我们给 assert! 宏提供一个布尔值参数。如果值为 true 则什么也不会发生测试通过。如果值为 false 则 assert! 宏调用 panic! 导致测试失败。使用 assert! 宏可以帮助我们检查代码是否按照我们的意图运行。 在第 5 章清单 5-15 中我们使用了 Rectangle 结构和 can_hold 方法在清单 11-5 中重复了这些代码。让我们把这些代码放到 src/lib.rs 文件中然后使用 assert! 宏编写一些测试。 #[derive(Debug)] struct Rectangle {width: u32,height: u32, }impl Rectangle {fn can_hold(self, other: Rectangle) - bool {self.width other.width self.height other.height} } (清单 11-5使用第 5 章中的 Rectangle 结构及其 can_hold 方法) can_hold 方法返回一个布尔值这意味着它是 assert! 宏的完美用例。在清单 11-6 中我们创建了一个宽度为 8、高度为 7 的 Rectangle 实例并断言它可以容纳另一个宽度为 5、高度为 1 的 Rectangle 实例从而编写了一个测试来练习 can_hold 方法。 #[cfg(test)] mod tests {use super::*;#[test]fn larger_can_hold_smaller() {let larger Rectangle {width: 8,height: 7,};let smaller Rectangle {width: 5,height: 1,};assert!(larger.can_hold(smaller));} } (清单 11-6 can_hold 的测试检查较大的矩形是否确实能容纳较小的矩形) 请注意我们在 tests 模块中添加了一行新内容 use super::*; . tests 模块是一个常规模块遵循第 7 章 模块树中引用项的路径 一节中介绍的常规可见性规则。由于 tests 模块是一个内层模块我们需要将外层模块中的被测代码引入内层模块的作用域。我们在此使用 glob这样外层模块中定义的任何内容都可以在 tests 模块中使用。 我们将测试命名为 larger_can_hold_smaller 并创建了我们需要的两个 Rectangle 实例。然后我们调用 assert! 宏并将调用 larger.can_hold(smaller) 的结果传给它。这个表达式应该返回 true 所以我们的测试应该会通过。让我们来看看 cargo.exe testCompiling adder v0.1.0 (E:\rustProj\adder)running 1 test test tests::larger_can_hold_smaller ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests adderrunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 确实通过了我们再添加一个测试这次断言一个较小的矩形不能容纳一个较大的矩形 #[derive(Debug)] struct Rectangle {width: u32,height: u32, }impl Rectangle {fn can_hold(self, other: Rectangle) - bool {self.width other.width self.height other.height} } #[cfg(test)] mod tests {use super::*;#[test]fn larger_can_hold_smaller() {let larger Rectangle {width: 8,height: 7,};let smaller Rectangle {width: 5,height: 1,};assert!(larger.can_hold(smaller));}#[test]fn smaller_cannot_hold_larger() {let larger Rectangle {width: 8,height: 7,};let smaller Rectangle {width: 5,height: 1,};assert!(!smaller.can_hold(larger));} } 由于 can_hold 函数在本例中的正确结果是 false 因此我们需要在将该结果传递给 assert! 宏之前对其进行否定。因此如果 can_hold 返回 false 我们的测试就会通过 cargo.exe testCompiling adder v0.1.0 (E:\rustProj\adder)running 2 tests test tests::larger_can_hold_smaller ... ok test tests::smaller_cannot_hold_larger ... oktest result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests adderrunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s两个测试通过现在让我们看看当我们在代码中引入一个错误时测试结果会发生什么变化。我们将改变 can_hold 方法的实现在比较宽度时用小于号代替大于号 impl Rectangle {fn can_hold(self, other: Rectangle) - bool {self.width other.width self.height other.height} } 现在运行测试结果如下 cargo.exe testCompiling adder v0.1.0 (E:\rustProj\adder)running 2 tests test tests::larger_can_hold_smaller ... FAILED test tests::smaller_cannot_hold_larger ... okfailures:---- tests::larger_can_hold_smaller stdout ---- thread tests::larger_can_hold_smaller panicked at src\lib.rs:28:9: assertion failed: larger.can_hold(smaller) note: run with RUST_BACKTRACE1 environment variable to display a backtracefailures:tests::larger_can_hold_smallertest result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: test failed, to rerun pass --lib 我们的测试发现了这个错误因为 larger.width 是 8而 smaller.width 是 5所以 can_hold 中宽度的比较现在返回 false : 8 不小于 5。 11.1.3 使用 assert_eq! 和 assert_ne! 宏测试等价性 验证功能的常用方法是测试被测代码的结果与您期望代码返回的值是否相等。您可以使用 assert! 宏并使用 运算符传递表达式。然而这是一个非常常见的测试因此标准库提供了一对宏 assert_eq! 和 assert_ne! 以更方便地执行该测试。这些宏分别比较两个参数是否相等或不相等。如果断言失败它们还会打印这两个值这样就能更容易地看出测试失败的原因相反 assert! 宏只指出它为 表达式得到了 false 值而没有打印导致 false 值的值。 在清单 11-7 中我们编写了一个名为 add_two 的函数在其参数中添加了 2 然后使用 assert_eq! 宏测试该函数。 pub fn add_two(a: i32) - i32 {a 2 }#[cfg(test)] mod tests {use super::*;#[test]fn it_adds_two() {assert_eq!(4, add_two(2));} } (清单 11-7使用 assert_eq! 宏测试函数 add_two) 检查一下是否通过 cargo.exe testCompiling adder v0.1.0 (E:\rustProj\adder)Finished test [unoptimized debuginfo] target(s) in 0.36sRunning unittests src\lib.rs (target\debug\deps\adder-033e40fcb4baf750.exe)running 1 test test tests::it_adds_two ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests adderrunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 我们将 4 作为 assert_eq! 的参数它等于调用 add_two(2) 的结果。该测试的行是 test tests::it_adds_two ... ok 而 ok 文本表示测试通过 让我们在代码中引入一个错误看看 assert_eq! 失败时的样子。更改 add_two 函数的实现改为添加 3 pub fn add_two(a: i32) - i32 {a 3 } 再次运行测试 cargo.exe testCompiling adder v0.1.0 (E:\rustProj\adder)Finished test [unoptimized debuginfo] target(s) in 0.38sRunning unittests src\lib.rs (target\debug\deps\adder-033e40fcb4baf750.exe)running 1 test test tests::it_adds_two ... FAILEDfailures:---- tests::it_adds_two stdout ---- thread tests::it_adds_two panicked at src\lib.rs:11:9: assertion left right failedleft: 4right: 5 note: run with RUST_BACKTRACE1 environment variable to display a backtracefailures:tests::it_adds_twotest result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: test failed, to rerun pass --lib 我们的测试发现了这个错误 it_adds_two 测试失败消息告诉我们失败的断言是 assertion failed: (left right) 以及 left 和 right 的值是多少。这条信息帮助我们开始调试 left 参数是 4 但 right 参数我们原来的 add_two(2) 是 5 。可以想象当我们进行大量测试时这条信息会特别有用。 请注意在某些语言和测试框架中相等断言函数的参数被称为 expected 和 actual 我们指定参数的顺序很重要。然而在 Rust 中它们被称为 left 和 right 我们指定期望值和代码生成值的顺序并不重要。我们可以将此测试中的断言写成 assert_eq!(add_two(2), 4) 这样就会出现与 assertion failed: (left right) 相同的失败消息。 如果我们给出的两个值不相等 assert_ne! 宏将通过如果相等则失败。当我们不确定数值会是什么但知道数值肯定不应该是什么时这个宏最有用。例如如果我们正在测试一个保证以某种方式改变输入的函数但改变输入的方式取决于我们运行测试的星期那么最好的断言可能是函数的输出不等于输入。 在表面上 assert_eq! 和 assert_ne! 宏分别使用运算符 和 ! 。当断言失败时这些宏会使用调试格式打印参数这意味着被比较的值必须实现 PartialEq 和 Debug 特性。所有基元类型和大多数标准库类型都实现了这些特性。对于您自己定义的结构体和枚举您需要实现 PartialEq 来断言这些类型的相等性。您还需要实现 Debug 以便在断言失败时打印值。正如第 5 章清单 5-12 所述由于这两种特质都是可派生的特质因此通常只需在结构或枚举定义中添加 #[derive(PartialEq, Debug)] 注解即可。 11.1.4 添加自定义故障信息 作为 assert! 、 assert_eq! 和 assert_ne! 宏的可选参数您还可以添加要与失败信息一起打印的自定义信息。在必备参数之后指定的任何参数都将传递给 format! 宏在第 8 章 使用 操作符或 format! 宏进行连接 一节中讨论因此可以传递包含 {} 占位符和要在这些占位符中使用的值的格式字符串。自定义消息对于记录断言的含义非常有用当测试失败时你可以更好地了解代码的问题所在。 例如我们有一个按姓名问候他人的函数我们想测试传入函数的姓名是否出现在输出中 pub fn greeting(name: str) - String {format!(Hello {}!, name) }#[cfg(test)] mod tests {use super::*;#[test]fn greeting_contains_name() {let result greeting(Carol);assert!(result.contains(Carol));} } 这个程序的要求尚未达成一致而且我们很确定 Hello 开头的问候语文本会发生变化。我们决定在需求发生变化时不必更新测试因此我们不检查 greeting 函数返回的值是否完全相等而只断言输出包含输入参数的文本。 现在让我们将 greeting 改为排除 name 在代码中引入一个错误看看默认的测试失败是什么样的 pub fn greeting(name: str) - String {String::from(Hello!) }运行该测试会产生以下结果 cargo.exe testCompiling adder v0.1.0 (E:\rustProj\adder)running 1 test test tests::greeting_contains_name ... FAILEDfailures:---- tests::greeting_contains_name stdout ---- thread tests::greeting_contains_name panicked at src\lib.rs:13:9: assertion failed: result.contains(Carol) note: run with RUST_BACKTRACE1 environment variable to display a backtracefailures:tests::greeting_contains_nametest result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: test failed, to rerun pass --lib 该结果只表明断言失败以及断言在哪一行。更有用的失败消息是打印 greeting 函数的值。让我们添加一条自定义的失败消息该消息由格式字符串组成其中的占位符填有我们从 greeting 函数得到的实际值 #[test] fn greeting_contains_name() {let result greeting(Carol);assert!(result.contains(Carol),Greeting did not contain name, value was {},result); } 现在当我们运行测试时会得到一条内容更丰富的错误信息 cargo.exe testCompiling adder v0.1.0 (E:\rustProj\adder)running 1 test test tests::greeting_contains_name ... FAILEDfailures:---- tests::greeting_contains_name stdout ---- thread tests::greeting_contains_name panicked at src\lib.rs:13:5: Greeting did not contain name, value was Hello! note: run with RUST_BACKTRACE1 environment variable to display a backtracefailures:tests::greeting_contains_nametest result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: test failed, to rerun pass --lib 我们可以在测试输出中看到实际得到的值这将有助于我们调试发生了什么而不是我们期望发生什么。 11.1.5 使用 should_panic检查panic异常 除了检查返回值外检查代码是否按照我们的预期处理错误条件也很重要。例如请看我们在第 9 章 清单 9-13 中创建的 Guess 类型。使用 Guess 的其他代码依赖于 Guess 实例只能包含 1 到 100 之间值的保证。我们可以编写一个测试以确保在尝试创建 Guess 实例时如果值超出了这个范围就会出错。 为此我们在测试函数中添加了属性 should_panic 。如果函数内的代码出现恐慌则测试通过如果函数内的代码没有出现恐慌则测试失败。 清单 11-8 显示了一个测试用于检查 Guess::new 的错误条件是否在我们预期的情况下发生。 pub struct Guess {value: i32, }impl Guess {pub fn new(value: i32) - Guess {if value 1 || value 100 {panic!(Guess value must be between 1 and 100, got {}., value);}Guess { value }} }#[cfg(test)] mod tests {use super::*;#[test]#[should_panic]fn greater_than_100() {Guess::new(200);} } (清单 11-8测试一个条件是否会导致 panic!) 我们将 #[should_panic] 属性放在 #[test] 属性之后测试函数之前。让我们看看测试通过后的结果 cargo.exe testCompiling adder v0.1.0 (E:\rustProj\adder)running 1 test test tests::greater_than_100 - should panic ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests adderrunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s看起来不错现在让我们在代码中引入一个错误删除 new 函数在值大于 100 时会出错的条件 impl Guess {pub fn new(value: i32) - Guess {if value 1 {panic!(Guess value must be between 1 and 100, got {}., value);}Guess { value }} } 当我们运行清单 11-8 中的测试时测试将失败 cargo.exe testCompiling adder v0.1.0 (E:\rustProj\adder)running 1 test test tests::greater_than_100 - should panic ... FAILEDfailures:---- tests::greater_than_100 stdout ---- note: test did not panic as expectedfailures:tests::greater_than_100test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: test failed, to rerun pass --lib 在这种情况下我们没有得到非常有用的信息但当我们查看测试函数时我们会发现它的注释是 #[should_panic] 。我们得到的失败信息意味着测试函数中的代码没有引起恐慌。 使用 should_panic 的测试可能并不精确。即使测试由于与我们预期不同的原因而慌乱 should_panic 测试也会通过。为了使 should_panic 测试更加精确我们可以在 should_panic 属性中添加一个可选的 expected 参数。测试线束将确保失败消息包含所提供的文本。例如请看清单 11-9 中 Guess 的修改代码其中 new 函数会根据值太小或太大而出现不同的提示信息。 pub struct Guess {value: i32, }impl Guess {pub fn new(value: i32) - Guess {if value 1 {panic!(Guess value must be greater than or equal to 1, got {}.,value);} else if value 100 {panic!(Guess value must be less than or equal to 100, got {}.,value);}Guess { value }} }#[cfg(test)] mod tests {use super::*;#[test]#[should_panic(expected less than or equal to 100)]fn greater_than_100() {Guess::new(200);} } (清单 11-9测试 panic! 是否包含指定子串的恐慌信息) 该测试将通过因为我们在 should_panic 属性的 expected 参数中输入的值是 Guess::new 函数恐慌信息的子串。我们也可以指定我们所期望的整个恐慌信息在本例中就是 Guess value must be less than or equal to 100, got 200. 。选择指定什么取决于恐慌信息有多少是唯一的或动态的以及您希望测试有多精确。在本例中一个恐慌信息子串就足以确保测试函数中的代码执行 else if value 100 例。 为了了解 should_panic 测试和 expected 消息失败后的情况让我们再次通过交换 if value 1 和 else if value 100 块的主体在代码中引入一个错误 if value 1 {panic!(Guess value must be less than or equal to 100, got {}.,value); } else if value 100 {panic!(Guess value must be greater than or equal to 1, got {}.,value); }这一次当我们运行 should_panic 测试时它将失败 cargo.exe testCompiling adder v0.1.0 (E:\rustProj\adder)running 1 test test tests::greater_than_100 - should panic ... FAILEDfailures:---- tests::greater_than_100 stdout ---- thread tests::greater_than_100 panicked at src\lib.rs:13:13: Guess value must be greater than or equal to 1, got 200. note: run with RUST_BACKTRACE1 environment variable to display a backtrace note: panic did not contain expected stringpanic message: Guess value must be greater than or equal to 1, got 200.,expected substring: less than or equal to 100failures:tests::greater_than_100test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: test failed, to rerun pass --lib 失败消息表明该测试确实如我们所料发生了恐慌但恐慌消息中并不包括预期的字符串 Guess value must be less than or equal to 100 。在这种情况下我们得到的恐慌信息是 Guess value must be greater than or equal to 1, got 200. 现在我们可以开始找出错误所在了 11.1.6 在测试中使用 ResultT, E 到目前为止我们的测试在失败时都会惊慌失措。我们还可以编写使用 ResultT, E 的测试下面是清单 11-1 中的测试重写后使用 ResultT, E 并返回 Err 而不是 panic #[cfg(test)] mod tests {#[test]fn it_works() - Result(), String {if 2 2 4 {Ok(())} else {Err(String::from(two plus two does not equal four))}} } it_works 函数现在具有 Result(), String 返回类型。在函数的主体中我们不调用 assert_eq! 宏而是在测试通过时返回 Ok(()) 在测试失败时返回内含 String 的 Err 。 在编写测试时如果测试返回 ResultT, E 就可以在测试正文中使用问号运算符这样就可以方便地编写测试如果测试中的任何操作返回 Err 变体测试就会失败。 不能在使用 ResultT, E 的测试中使用 #[should_panic] 注释。要断言操作返回的是 Err 变体不要在 ResultT, E 值上使用问号运算符。相反请使用 assert!(value.is_err()) 。 现在您已经知道了编写测试的几种方法让我们来看看运行测试时会发生什么并探索 cargo test 中的不同选项。 11.2 控制测试运行方式 正如 cargo run 会编译代码并运行生成的二进制文件一样 cargo test 也会在测试模式下编译代码并运行生成的测试二进制文件。 cargo test 生成的二进制文件的默认行为是并行运行所有测试并捕获测试运行过程中产生的输出防止显示输出从而更容易读取与测试结果相关的输出。不过您可以指定命令行选项来更改默认行为。 有些命令行选项会进入 cargo test 有些会进入生成的测试二进制文件。要区分这两类参数可以列出进入 cargo test 的参数然后是分隔符 -- 最后是进入测试二进制文件的参数。运行 cargo test --help 会显示可以与 cargo test 一起使用的选项运行 cargo test -- --help 会显示可以在分隔符之后使用的选项。 11.2.1 并行或连续运行测试 当你运行多个测试时默认情况下它们会使用线程并行运行这意味着它们会更快地完成运行你也能更快地得到反馈。由于测试是同时运行的因此必须确保测试不相互依赖也不依赖任何共享状态包括共享环境如当前工作目录或环境变量。 例如每个测试都运行一些代码在磁盘上创建名为 test-output.txt 的文件并向该文件写入一些数据。然后每个测试都会读取该文件中的数据并断言该文件包含一个特定的值而每个测试中的值都是不同的。由于测试是同时运行的一个测试可能会在另一个测试写入和读取文件之间的时间内覆盖文件。这样第二个测试就会失败不是因为代码不正确而是因为测试在并行运行时相互干扰。一种解决方案是确保每个测试都写入不同的文件另一种解决方案是一次运行一个测试。 如果不想并行运行测试或者想对使用的线程数进行更精细的控制可以向测试二进制文件发送 --test-threads 标志和希望使用的线程数。请看下面的示例 $ cargo test -- --test-threads1我们将测试线程数设置为 1 告诉程序不要使用任何并行方式。使用一个线程运行测试会比并行运行花费更多时间但如果测试共享状态则不会相互干扰。 11.2.2 显示功能输出 默认情况下如果测试通过Rust 的测试库会捕获打印到标准输出的任何内容。例如如果我们在测试中调用 println! 且测试通过我们不会在终端中看到 println! 的输出我们只会看到表明测试通过的一行。如果测试失败我们将看到打印到标准输出的内容以及失败信息的其余部分。 例如清单 11-10 中有一个傻函数它打印参数值并返回 10还有一个测试通过和一个测试失败。 fn prints_and_returns_10(a: i32) - i32 {println!(I got the value {}, a);10 }#[cfg(test)] mod tests {use super::*;#[test]fn this_test_will_pass() {let value prints_and_returns_10(4);assert_eq!(10, value);}#[test]fn this_test_will_fail() {let value prints_and_returns_10(8);assert_eq!(5, value);} } (清单 11-10对调用 println!) 当我们使用 cargo test 运行这些测试时会看到如下输出 cargo.exe testCompiling adder v0.1.0 (E:\rustProj\adder)running 2 tests test tests::this_test_will_fail ... FAILED test tests::this_test_will_pass ... ok failures:---- tests::this_test_will_fail stdout ---- I got the value 8 thread tests::this_test_will_fail panicked at src\lib.rs:19:9: assertion left right failedleft: 5right: 10 note: run with RUST_BACKTRACE1 environment variable to display a backtracefailures:tests::this_test_will_failtest result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: test failed, to rerun pass --lib 请注意在该输出中我们看不到 I got the value 4 而这正是通过的测试运行时的打印输出。该输出已被捕获。失败测试的输出 I got the value 8 出现在测试摘要输出部分该部分还显示了测试失败的原因。 如果我们还想查看通过测试的打印值可以通过 --show-output 告诉 Rust 也显示成功测试的输出。 cargo test -- --show-output 当我们使用 --show-output 标志再次运行清单 11-10 中的测试时会看到以下输出结果 cargo.exe test -- --show-outputrunning 2 tests test tests::this_test_will_fail ... FAILED test tests::this_test_will_pass ... oksuccesses:---- tests::this_test_will_pass stdout ---- I got the value 4successes:tests::this_test_will_passfailures:---- tests::this_test_will_fail stdout ---- I got the value 8 thread tests::this_test_will_fail panicked at src\lib.rs:19:9: assertion left right failedleft: 5right: 10 note: run with RUST_BACKTRACE1 environment variable to display a backtracefailures:tests::this_test_will_failtest result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: test failed, to rerun pass --lib 11.2.3 按名称运行测试子集 有时运行一个完整的测试套件需要很长时间。如果你正在处理某一特定领域的代码你可能只想运行与该代码相关的测试。您可以将 cargo test 作为参数传递给要运行的测试名称从而选择要运行的测试。 为了演示如何运行测试子集我们首先要为 add_two 函数创建三个测试如清单 11-11 所示然后选择要运行的测试。 pub fn add_two(a: i32) - i32 {a 2 }#[cfg(test)] mod tests {use super::*;#[test]fn add_two_and_two() {assert_eq!(4, add_two(2));}#[test]fn add_three_and_two() {assert_eq!(5, add_two(3));}#[test]fn one_hundred() {assert_eq!(102, add_two(100));} } 如果我们运行测试时不传递任何参数如前所述所有测试都将并行运行 cargo.exe test Compiling adder v0.1.0 (E:\rustProj\adder)Finished test [unoptimized debuginfo] target(s) in 0.42sRunning unittests src\lib.rs (target\debug\deps\adder-033e40fcb4baf750.exe)running 3 tests test tests::add_three_and_two ... ok test tests::add_two_and_two ... ok test tests::one_hundred ... oktest result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests adderrunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 11.2.3.1 运行单项测试 我们可以将任何测试函数的名称传递给 cargo test 以便只运行该测试 cargo.exe test one_hundredFinished test [unoptimized debuginfo] target(s) in 0.00sRunning unittests src\lib.rs (target\debug\deps\adder-033e40fcb4baf750.exe)running 1 test test tests::one_hundred ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s 只有名称为 one_hundred 的测试运行了其他两个测试与名称不符。测试输出在最后显示 2 filtered out 让我们知道还有更多测试没有运行。 我们不能以这种方式指定多个测试的名称只有给 cargo test 的第一个值会被使用。但有一种方法可以运行多个测试。 11.2.3.2 过滤以运行多个测试 我们可以指定测试名称的一部分然后运行名称与该值相匹配的任何测试。例如由于有两个测试的名称包含 add 我们可以通过运行 cargo test add 来运行这两个测试 cargo.exe test add Finished test [unoptimized debuginfo] target(s) in 0.00sRunning unittests src\lib.rs (target\debug\deps\adder-033e40fcb4baf750.exe)running 2 tests test tests::add_three_and_two ... ok test tests::add_two_and_two ... oktest result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s 这条命令运行了名称中包含 add 的所有测试并过滤掉了名为 one_hundred 的测试。还要注意的是测试所在的模块会成为测试名称的一部分因此我们可以通过过滤模块名称来运行模块中的所有测试。 11.2.4 除非特别要求否则忽略某些测试 有时执行一些特定测试会非常耗时因此您可能希望在运行 cargo test 时排除这些测试。您可以使用 ignore 属性注释耗时测试以排除这些测试而不是将所有要运行的测试作为参数列出如图所示 #[test] fn it_works() {assert_eq!(2 2, 4); }#[test] #[ignore] fn expensive_test() {// code that takes an hour to run } 在 #[test] 之后我们在要排除的测试中添加 #[ignore] 行。现在当我们运行测试时 it_works 会运行但 expensive_test 不会 cargo.exe test Compiling adder v0.1.0 (E:\rustProj\adder)Finished test [unoptimized debuginfo] target(s) in 0.41sRunning unittests src\lib.rs (target\debug\deps\adder-033e40fcb4baf750.exe)running 2 tests test expensive_test ... ignored test it_works ... oktest result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests adderrunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s expensive_test 函数被列为 ignored 。如果我们只想运行被忽略的测试可以使用 cargo test -- --ignored cargo.exe test -- --ignoredFinished test [unoptimized debuginfo] target(s) in 0.00sRunning unittests src\lib.rs (target\debug\deps\adder-033e40fcb4baf750.exe)running 1 test test expensive_test ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00sDoc-tests adderrunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 通过控制运行哪些测试您可以确保 cargo test 的结果是快速的。当您需要检查 ignored 测试结果并且有时间等待结果时可以运行 cargo test -- --ignored 。如果想运行所有测试无论它们是否被忽略可以运行 cargo test -- --include-ignored 。 11.3 测试组织 正如本章开头提到的测试是一门复杂的学科不同的人使用不同的术语和组织方式。Rust 社区将测试分为两大类单元测试和集成测试。单元测试的规模较小重点更突出一次只测试一个独立的模块并且可以测试私有接口。集成测试则完全不依赖于你的库使用你的代码的方式与其他外部代码相同只使用公共接口每次测试可能会测试多个模块。 编写这两种测试对于确保程序库的各个部分都能按照您的期望单独或共同完成测试非常重要。 11.3.1 单元测试 单元测试的目的是将每个单元的代码与其他代码隔离开进行测试以便快速确定代码是否按预期运行。你需要将单元测试放在 src 目录下的每个文件中并对其代码进行测试。惯例是在每个文件中创建一个名为 tests 的模块来包含测试函数并在模块中注释 cfg(test) 。 11.3.1.1 测试模块和 #[cfg(test)] 测试模块上的 #[cfg(test)] 注解告诉 Rust只有在运行 cargo test 时才编译和运行测试代码而不是运行 cargo build 时。当你只想编译库时这样做可以节省编译时间而且由于不包含测试编译后的工件也可以节省空间。你会发现由于集成测试放在不同的目录下因此它们不需要 #[cfg(test)] 注释。然而由于单元测试与代码位于相同的文件中因此您需要使用 #[cfg(test)] 来指定编译结果中不包含单元测试。 回想一下在本章第一节生成新的 adder 项目时Cargo 为我们生成了这段代码 #[cfg(test)] mod tests {#[test]fn it_works() {let result 2 2;assert_eq!(result, 4);} } 这段代码就是自动生成的测试模块。属性 cfg 代表配置它告诉 Rust只有在特定的配置选项下才应包含以下项目。在本例中配置选项是 test 它由 Rust 提供用于编译和运行测试。通过使用 cfg 属性Cargo 只会在我们使用 cargo test 运行测试时编译我们的测试代码。除了注释为 #[test] 的函数外这还包括该模块中可能存在的任何辅助函数。 11.3.1.2 测试私有函数 测试界对是否应该直接测试私有函数存在争议而其他语言则很难或根本不可能测试私有函数。无论你遵循哪种测试思想Rust 的隐私规则确实允许你测试私有函数。请看清单 11-12 中的代码其中包含私有函数 internal_adder 。 pub fn add_two(a: i32) - i32 {internal_adder(a, 2) }fn internal_adder(a: i32, b: i32) - i32 {a b }#[cfg(test)] mod tests {use super::*;#[test]fn internal() {assert_eq!(4, internal_adder(2, 2));} } (清单 11-12测试私有函数) 注意 internal_adder 函数没有标记为 pub 。测试只是 Rust 代码而 tests 模块只是另一个模块。正如我们在 在模块树中引用项的路径 一节中所讨论的子模块中的项可以使用其祖先模块中的项。在本测试中我们通过 use super::* 将 test 模块父模块中的所有项引入作用域然后测试就可以调用 internal_adder 。如果您认为不应该测试私有函数Rust 中也没有强制要求您这样做。 11.3.2 集成测试 在 Rust 中集成测试完全处于库的外部。它们使用库的方式与其他代码相同这意味着它们只能调用属于库公共 API 的函数。集成测试的目的是测试库中的许多部分是否能正确地协同工作。单独运行正常的代码单元在集成时可能会出现问题因此集成代码的测试覆盖范围也很重要。要创建集成测试首先需要一个test目录。 11.3.2.1 test目录 我们在项目目录的顶层 src 旁边创建了一个测试目录。Cargo 知道要在这个目录中查找集成测试文件。然后我们可以创建任意数量的测试文件Cargo 会将每个文件编译为一个独立的 crate。 让我们创建一个集成测试。将清单 11-12 中的代码保留在 src/lib.rs 文件中创建一个测试目录并创建一个名为 tests/integration_test.rs 的新文件。目录结构应如下所示 │ .gitignore │ Cargo.lock │ Cargo.toml │ ├─src │ lib.rs │ └─testsintegration_test.rs 在 tests/integration_test.rs 文件中输入清单 11-13 中的代码 use adder;#[test] fn it_adds_two() {assert_eq!(4, adder::add_two(2)); } (清单 11-13 adder crate 中一个函数的集成测试) tests 目录中的每个文件都是一个独立的板块因此我们需要将我们的库引入每个测试板块的范围。因此我们在代码顶部添加了 use adder 而单元测试中并不需要。 我们不需要在 tests/integration_test.rs 中用 #[cfg(test)] 注释任何代码。Cargo 会对 tests 目录进行特殊处理只有在运行 cargo test 时才会编译该目录下的文件。现在运行 cargo test cargo.exe testCompiling adder v0.1.0 (E:\rustProj\adder)Finished test [unoptimized debuginfo] target(s) in 0.58sRunning unittests src\lib.rs (target\debug\deps\adder-033e40fcb4baf750.exe)running 1 test test tests::internal ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sRunning tests\integration_test.rs (target\debug\deps\integration_test-2bac9098cab66043.exe)running 1 test test it_adds_two ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests adderrunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 输出的三个部分包括单元测试、集成测试和文档测试。请注意如果某个部分的任何测试失败后面的部分将不会运行。例如如果单元测试失败集成测试和文档测试就不会有任何输出因为这些测试只有在所有单元测试都通过时才会运行。 单元测试的第一部分与我们一直看到的相同每项单元测试一行清单 11-12 中添加的名为 internal 的一行然后是单元测试的摘要行。 集成测试部分以 Running tests/integration_test.rs 开头。接下来集成测试中的每个测试功能都有一行在 Doc-tests adder 部分开始之前还有一行集成测试结果摘要。 每个集成测试文件都有自己的部分因此如果我们在测试目录中添加更多文件就会有更多的集成测试部分。 我们仍然可以通过指定测试功能的名称作为 cargo test 的参数来运行特定的集成测试功能。要运行特定集成测试文件中的所有测试请使用 --test 参数 cargo test 然后在后面加上文件名 cargo.exe test --test integration_testFinished test [unoptimized debuginfo] target(s) in 0.00sRunning tests\integration_test.rs (target\debug\deps\integration_test-2bac9098cab66043.exe)running 1 test test it_adds_two ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 该命令只运行 tests/integration_test.rs 文件中的测试。 11.3.2.2 集成测试中的子模块 当你添加更多集成测试时你可能想在测试目录中创建更多文件来帮助组织它们例如你可以按测试功能对测试函数进行分组。如前所述测试目录中的每个文件都会编译为独立的 crate这有助于创建独立的作用域从而更接近最终用户使用 crate 的方式。不过这意味着测试目录中的文件与 src 目录中的文件不具有相同的行为正如第 7 章中关于如何将代码分隔为模块和文件所学到的那样。 当你在多个集成测试文件中使用一组辅助函数并尝试按照第 7 章 将模块分离到不同文件 一节中的步骤将它们提取到一个共同的模块中时测试目录文件的不同行为就最明显了。例如如果我们创建了 tests/common.rs并在其中放置了一个名为 setup 的函数我们就可以在 setup 中添加一些代码以便在多个测试文件中通过多个测试函数调用这些代码 pub fn setup() {// setup code specific to your librarys tests would go here } 当我们再次运行测试时我们会在 common.rs 文件的测试输出中看到一个新的部分尽管该文件不包含任何测试函数我们也没有在任何地方调用 setup 函数 cargo.exe test Compiling adder v0.1.0 (E:\rustProj\adder)Finished test [unoptimized debuginfo] target(s) in 0.67sRunning unittests src\lib.rs (target\debug\deps\adder-033e40fcb4baf750.exe)running 1 test test tests::internal ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sRunning tests\common.rs (target\debug\deps\common-0d6058881b615884.exe)running 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running tests\integration_test.rs (target\debug\deps\integration_test-2bac9098cab66043.exe)running 1 test test it_adds_two ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests adderrunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 让 common 出现在测试结果中并显示 running 0 tests 并不是我们想要的。我们只是想与其他集成测试文件共享一些代码。 为避免 common 出现在测试输出中我们将不创建 tests/common.rs而是创建 tests/common/mod.rs。现在项目目录如下 │ .gitignore │ Cargo.lock │ Cargo.toml │ ├─src │ lib.rs │ └─tests│ integration_test.rs│└─commonmod.rs 这是我们在第 7 章 替代文件路径 一节中提到的 Rust 也能理解的旧命名约定。以这种方式命名文件可以避免 Rust 将 common 模块视为集成测试文件。当我们将 setup 函数代码移入 tests/common/mod.rs 并删除 tests/common.rs 文件时测试输出中将不再出现该部分。测试目录下子目录中的文件不会被编译为单独的crate也不会在测试输出中出现章节。 创建 test/common/mod.rs 后我们就可以在任何集成测试文件中将其作为模块使用。下面是一个从 test/integration_test.rs 中的 it_adds_two 测试调用 setup 函数的示例 use adder;mod common;#[test] fn it_adds_two() {common::setup();assert_eq!(4, adder::add_two(2)); } 请注意 mod common; 声明与清单 7-21 中演示的模块声明相同。然后在测试函数中我们可以调用 common::setup() 函数。 11.3.2.3 二进制crate集成测试 如果我们的项目是一个二进制板块只包含一个 src/main.rs 文件而没有 src/lib.rs 文件我们就无法在测试目录下创建集成测试也无法通过 use 语句将 src/main.rs 文件中定义的函数引入作用域。只有库crate才会暴露其他板块可以使用的函数二进制crate应独立运行。 这也是提供二进制文件的 Rust 项目有一个直接的 src/main.rs 文件的原因之一该文件调用 src/lib.rs 文件中的逻辑。利用这种结构集成测试可以通过 use 测试库板块使重要功能可用。如果重要功能正常工作那么 src/main.rs 文件中的少量代码也将正常工作而这少量代码无需测试。 Rust 的测试功能提供了一种指定代码应如何运行的方法以确保即使在你进行修改时代码也能按照你的预期继续运行。单元测试可分别测试库的不同部分并可测试私有实现细节。集成测试则检查库中的许多部分是否能正确地协同工作它们使用库的公共应用程序接口以与外部代码相同的方式测试代码。尽管 Rust 的类型系统和所有权规则有助于防止某些类型的错误但测试对于减少与代码预期行为有关的逻辑错误仍然很重要。 下一篇 12-输入/输出项目构建命令行程序
http://www.yingshimen.cn/news/4021/

相关文章:

  • 建设内部网站目的wordpress需要先安装数据库
  • 铁岭哪家做营销型网站广东工厂网站建设
  • 合肥做淘宝网站深圳办公室装修公司哪家好
  • 赢了网站怎么做的深圳高端网页设计公司
  • 宜春做网站网络营销师
  • php做二手商城网站源码重庆网站建设平台
  • 长沙做网站的网页设计高清素材
  • 营站快车代理平台学校部门网站的建设
  • 网站页脚信息做网站东莞选哪家公司好
  • 四会市城乡规划建设局网站做静态网站的步骤
  • 俄文网站建设方案建站属于什么行业
  • 广州自助网站搭建制作公司wordpress主题 推荐
  • 做网站 excel安徽省建设协会网站
  • 网站友链查询搜索引擎seo
  • 德州市住房建设局网站wordpress速度很慢
  • 网站建设的相关职位如何做网页广告链接
  • 营销型网站是什么嘉兴网站制作
  • 二级目录网站怎么做摄影作品网站建设方案书
  • asp.net 做网站实例网站config配置教程
  • 医院网站如何备案yw开头的网络黄页
  • 湛江网站建设公司wordpress 破解主题下载
  • 做网站语言知乎合江网站建设
  • 网站建设百度经验dede网站地图路径修改
  • 国内优秀网站赏析除了亚马逊还有啥网站做海淘
  • 专业的食品行业网站开发html在线编辑器预览网页版
  • 做网站的知识哪个网站是用vue做的
  • 企业摄影网站模板广州冼村属于哪个区
  • 国内创意产品网站个人网站制作论文
  • 韩雪冬个人网站 北京网页制作的公司多少收入
  • 美橙互联网站建设案例公司网站开发技巧