简介
欢迎来到如何编写测试。本实验是《Rust 程序设计语言》的一部分。你可以在 LabEx 中练习 Rust 技能。
在本实验中,我们将学习如何使用属性、宏和断言在 Rust 中编写测试。
如何编写测试
测试是 Rust 函数,用于验证非测试代码是否按预期方式运行。测试函数的主体通常执行以下三个操作:
- 设置任何所需的数据或状态。
- 运行你要测试的代码。
- 断言结果符合你的预期。
让我们看看 Rust 专门为编写执行这些操作的测试而提供的特性,其中包括 test 属性、一些宏以及 should_panic 属性。
测试函数剖析
在 Rust 中,最简单的测试就是一个使用 test 属性进行注解的函数。属性是关于 Rust 代码片段的元数据;比如我们在第五章用于结构体的 derive 属性。要将一个函数变成测试函数,在 fn 之前的那一行添加 #[test]。当你使用 cargo test 命令运行测试时,Rust 会构建一个测试运行器二进制文件,它会运行这些带注解的函数,并报告每个测试函数是通过还是失败。
每当我们使用 Cargo 创建一个新的库项目时,会自动为我们生成一个包含测试函数的测试模块。这个模块为你提供了一个编写测试的模板,这样你每次开始一个新项目时就不必去查找确切的结构和语法了。你可以根据需要添加任意数量的额外测试函数和测试模块!
在实际测试任何代码之前,我们先通过对模板测试进行实验来探索测试的一些工作原理。然后我们将编写一些实际的测试,调用我们编写的一些代码,并断言其行为是正确的。
让我们创建一个名为 adder 的新库项目,用于将两个数字相加:
$ cargo new adder --lib
Created library $(adder) project
$ cd adder
你的 adder 库中 src/lib.rs 文件的内容应该如下所示(清单 11 - 1)。
文件名:src/lib.rs
#[cfg(test)]
mod tests {
1 #[test]
fn it_works() {
let result = 2 + 2;
2 assert_eq!(result, 4);
}
}
清单 11 - 1:由 cargo new 自动生成的测试模块和函数
目前,让我们先忽略前两行,专注于这个函数。注意 #[test] 注解 [1]:这个属性表明这是一个测试函数,这样测试运行器就知道将这个函数视为测试。在 tests 模块中我们可能也有非测试函数,用于帮助设置常见场景或执行常见操作,所以我们总是需要指明哪些函数是测试。
示例函数体使用 assert_eq! 宏 [2] 来断言 result(它包含 2 加 2 的结果)等于 4。这个断言是典型测试格式的一个示例。让我们运行它看看这个测试是否通过。
cargo test 命令会运行我们项目中的所有测试,如清单 11 - 2 所示。
[object Object]
清单 11 - 2:运行自动生成的测试的输出
Cargo 编译并运行了测试。我们看到 running 1 test 这一行 [1]。下一行显示了生成的测试函数的名称,即 it_works,并且运行该测试的结果是 ok [2]。整体总结 test result: ok. [3] 表示所有测试都通过了,而 1 passed; 0 failed 这部分汇总了通过或失败的测试数量。
可以将一个测试标记为被忽略,这样它在特定实例中就不会运行;我们将在“除非特别请求,否则忽略某些测试”中介绍这一点。因为我们这里没有这样做,所以总结中显示 0 ignored。我们也可以向 cargo test 命令传递一个参数,只运行名称与某个字符串匹配的测试;这称为 _过滤_,我们将在“按名称运行测试子集”中介绍。这里我们没有过滤正在运行的测试,所以总结的最后显示 0 filtered out。
0 measured 统计信息是关于测量性能的基准测试的。截至本文撰写时,基准测试仅在 Rust 的夜间版本中可用。有关基准测试的更多信息,请参阅 https://doc.rust-lang.org/unstable-book/library-features/test.html 上的文档。
测试输出从 Doc-tests adder 开始的下一部分 [4] 是关于任何文档测试的结果。我们目前还没有任何文档测试,但是 Rust 可以编译我们 API 文档中出现的任何代码示例。这个功能有助于使你的文档和代码保持同步!我们将在“作为测试的文档注释”中讨论如何编写文档测试。目前,我们将忽略 Doc-tests 输出。
让我们开始根据自己的需要定制测试。首先,将 it_works 函数的名称更改为其他名称,比如 exploration,如下所示:
文件名:src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn exploration() {
let result = 2 + 2;
assert_eq!(result, 4);
}
}
然后再次运行 cargo test。现在输出显示的是 exploration 而不是 it_works:
running 1 test
test tests::exploration... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s
现在我们再添加一个测试,但这次我们要让这个测试失败!当测试函数中的某些内容导致程序恐慌时,测试就会失败。每个测试都在一个新线程中运行,当主线程看到一个测试线程终止时,该测试就会被标记为失败。在第九章中,我们讨论过导致程序恐慌的最简单方法是调用 panic! 宏。输入一个名为 another 的新测试函数,这样你的 src/lib.rs 文件就如下所示(清单 11 - 3)。
文件名:src/lib.rs
#[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 测试失败了。
running 2 tests
test tests::exploration... ok
1 test tests::another... FAILED
2 failures:
---- tests::another stdout ----
thread'main' panicked at 'Make this test fail', src/lib.rs:10:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace
3 failures:
tests::another
4 test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s
error: test failed, to rerun pass '--lib'
清单 11 - 4:一个测试通过而一个测试失败时的测试结果
test tests::another 这一行显示的不是 ok,而是 FAILED [1]。在单个测试结果和总结之间出现了两个新部分:第一个 [2] 显示了每个测试失败的详细原因。在这种情况下,我们得到的详细信息是 another 失败了,因为它在 src/lib.rs 文件的第 10 行 panicked at 'Make this test fail'。下一个部分 [3] 只列出了所有失败测试的名称,当有很多测试和大量详细的失败测试输出时,这很有用。我们可以使用失败测试的名称来只运行那个测试,以便更轻松地调试它;我们将在“控制测试的运行方式”中更多地讨论运行测试的方法。
总结行在最后显示 [4]:总体而言,我们的测试结果是 FAILED。我们有一个测试通过,一个测试失败。
既然你已经看到了不同场景下的测试结果是什么样的,让我们来看看除了 panic! 之外,在测试中还有用的其他一些宏。
使用 assert! 宏检查结果
标准库提供的 assert! 宏,在你想要确保测试中的某个条件求值为 true 时非常有用。我们给 assert! 宏一个求值为布尔值的参数。如果该值为 true,则什么都不会发生,测试通过。如果该值为 false,assert! 宏会调用 panic! 使测试失败。使用 assert! 宏有助于我们检查代码是否按预期方式运行。
在清单 5 - 15 中,我们使用了一个 Rectangle 结构体和一个 can_hold 方法,清单 11 - 5 中再次给出了这些代码。让我们把这段代码放到 src/lib.rs 文件中,然后使用 assert! 宏为它编写一些测试。
文件名:src/lib.rs
#[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:使用第五章中的 Rectangle 结构体及其 can_hold 方法
can_hold 方法返回一个布尔值,这意味着它是使用 assert! 宏的完美用例。在清单 11 - 6 中,我们编写了一个测试,通过创建一个宽度为 8、高度为 7 的 Rectangle 实例,并断言它可以容纳另一个宽度为 5、高度为 1 的 Rectangle 实例,来测试 can_hold 方法。
文件名:src/lib.rs
#[cfg(test)]
mod tests {
1 use super::*;
#[test]
2 fn larger_can_hold_smaller() {
3 let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
4 assert!(larger.can_hold(&smaller));
}
}
清单 11 - 6:对 can_hold 的测试,检查一个较大的矩形是否确实能容纳一个较小的矩形
注意,我们在 tests 模块中添加了新的一行:use super::*; [1]。tests 模块是一个常规模块,遵循我们在“模块树中引用项的路径”中介绍的常见可见性规则。因为 tests 模块是一个内部模块,我们需要将外部模块中要测试的代码引入到内部模块的作用域中。我们在这里使用了通配符,所以我们在外部模块中定义的任何内容对这个 tests 模块都是可用的。
我们将测试命名为 larger_can_hold_smaller [2],并创建了所需的两个 Rectangle 实例 [3]。然后我们调用 assert! 宏,并将调用 larger.can_hold(&smaller) 的结果作为参数传递给它 [4]。这个表达式应该返回 true,所以我们的测试应该通过。让我们来验证一下!
running 1 test
test tests::larger_can_hold_smaller... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s
它确实通过了!让我们再添加一个测试,这次断言一个较小的矩形不能容纳一个较大的矩形:
文件名:src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
--snip--
}
#[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,我们的测试就会通过:
running 2 tests
test tests::larger_can_hold_smaller... ok
test tests::smaller_cannot_hold_larger... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s
两个测试都通过了!现在让我们看看当我们在代码中引入一个错误时,测试结果会发生什么。我们将通过在比较宽度时把大于号替换为小于号来更改 can_hold 方法的实现:
--snip--
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width < other.width && self.height > other.height
}
}
现在运行测试会产生以下结果:
running 2 tests
test tests::smaller_cannot_hold_larger... ok
test tests::larger_can_hold_smaller... FAILED
failures:
---- tests::larger_can_hold_smaller stdout ----
thread'main' panicked at 'assertion failed:
larger.can_hold(&smaller)', src/lib.rs:28:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace
failures:
tests::larger_can_hold_smaller
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s
我们的测试发现了这个错误!因为 larger.width 是 8,smaller.width 是 5,所以 can_hold 中宽度的比较现在返回 false:8 不小于 5。
使用 assert_eq! 和 assert_ne! 宏测试相等性
验证功能的一种常见方法是测试被测代码的结果与你期望代码返回的值之间是否相等。你可以通过使用 assert! 宏并向其传递一个使用 == 运算符的表达式来做到这一点。然而,这是一个非常常见的测试,以至于标准库提供了一对宏——assert_eq! 和 assert_ne!——来更方便地执行此测试。这些宏分别比较两个参数是否相等或不相等。如果断言失败,它们还会打印这两个值,这使得更容易看出测试失败的 _原因_;相反,assert! 宏仅表明它对于 == 表达式得到了一个 false 值,而不会打印导致该 false 值的那些值。
在清单 11 - 7 中,我们编写了一个名为 add_two 的函数,它将其参数加 2,然后我们使用 assert_eq! 宏来测试这个函数。
文件名:src/lib.rs
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 函数
让我们检查一下它是否通过!
running 1 test
test tests::it_adds_two... ok
test result: ok. 1 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
}
再次运行测试:
running 1 test
test tests::it_adds_two... FAILED
failures:
---- tests::it_adds_two stdout ----
1 thread'main' panicked at 'assertion failed: `(left == right)`
2 left: `4`,
3 right: `5`', src/lib.rs:11:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace
failures:
tests::it_adds_two
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s
我们的测试发现了这个错误!it_adds_two 测试失败了,并且消息告诉我们失败的断言是 assertion failed: (left == right)`\[1\] 以及left\[2\] 和right \[3\] 的值是什么。这个消息有助于我们开始调试: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 - 12 中所述,因为这两个特性都是可派生特性,所以这通常就像在你的结构体或枚举定义中添加 #[derive(PartialEq, Debug)] 注解一样简单。有关这些和其他可派生特性的更多详细信息,请参阅附录 C。
添加自定义失败消息
你还可以添加一条自定义消息,与失败消息一起打印,作为 assert!、assert_eq! 和 assert_ne! 宏的可选参数。在必选参数之后指定的任何参数都会被传递给 format! 宏(在“使用 + 运算符或 format! 宏进行拼接”中讨论),所以你可以传递一个包含 {} 占位符以及要放入这些占位符的值的格式字符串。自定义消息对于记录断言的含义很有用;当测试失败时,你将能更好地了解代码出了什么问题。
例如,假设我们有一个按名字向人们打招呼的函数,并且我们想要测试我们传递给该函数的名字是否出现在输出中:
文件名:src/lib.rs
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!")
}
运行这个测试会产生以下结果:
running 1 test
test tests::greeting_contains_name... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread'main' panicked at 'assertion failed:
result.contains(\"Carol\")', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace
failures:
tests::greeting_contains_name
这个结果只是表明断言失败了以及断言在第几行。一个更有用的失败消息会打印 greeting 函数返回的值。让我们添加一条自定义失败消息,它由一个格式字符串组成,其中的占位符用我们从 greeting 函数实际得到的值填充:
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{result}`"
);
}
现在当我们运行测试时,我们将得到一个更有信息量的错误消息:
---- tests::greeting_contains_name stdout ----
thread'main' panicked at 'Greeting did not contain name, value
was `Hello!`', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace
我们可以在测试输出中看到我们实际得到的值,这将帮助我们调试实际发生了什么,而不是我们期望发生什么。
使用 should_panic 检查是否发生恐慌
除了检查返回值之外,检查我们的代码是否按预期处理错误情况也很重要。例如,考虑我们在清单 9 - 13 中创建的 Guess 类型。其他使用 Guess 的代码依赖于 Guess 实例将只包含 1 到 100 之间的值这一保证。我们可以编写一个测试来确保尝试创建一个值超出该范围的 Guess 实例时会导致程序恐慌。
我们通过在测试函数上添加 should_panic 属性来做到这一点。如果函数内部的代码导致程序恐慌,则测试通过;如果函数内部的代码没有导致程序恐慌,则测试失败。
清单 11 - 8 展示了一个测试,用于检查 Guess::new 的错误情况是否在我们期望的时候发生。
// src/lib.rs
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:测试一个条件是否会导致程序恐慌!
我们将 #[should_panic] 属性放在 #[test] 属性之后,以及它所应用的测试函数之前。让我们看看这个测试通过时的结果:
running 1 test
test tests::greater_than_100 - should panic... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s
看起来不错!现在让我们通过移除 new 函数中当值大于 100 时会导致程序恐慌的条件来在代码中引入一个错误:
// src/lib.rs
--snip--
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 中的测试时,它将会失败:
running 1 test
test tests::greater_than_100 - should panic... FAILED
failures:
---- tests::greater_than_100 stdout ----
note: test did not panic as expected
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s
在这种情况下,我们没有得到一个非常有用的消息,但是当我们查看测试函数时,我们会看到它被标注了 #[should_panic]。我们得到的失败结果意味着测试函数中的代码没有导致程序恐慌。
使用 should_panic 的测试可能不够精确。即使测试因为与我们预期不同的原因而导致程序恐慌,should_panic 测试也会通过。为了使 should_panic 测试更精确,我们可以向 should_panic 属性添加一个可选的 expected 参数。测试框架将确保失败消息包含提供的文本。例如,考虑清单 11 - 9 中 Guess 的修改后的代码,其中 new 函数根据值是太小还是太大而以不同的消息导致程序恐慌。
// src/lib.rs
--snip--
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 这个分支。
为了看看当一个带有 expected 消息的 should_panic 测试失败时会发生什么,让我们再次通过交换 if value < 1 和 else if value > 100 块的主体来在代码中引入一个错误:
// src/lib.rs
--snip--
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
);
}
--snip--
这次当我们运行 should_panic 测试时,它将会失败:
running 1 test
test tests::greater_than_100 - should panic... FAILED
failures:
---- tests::greater_than_100 stdout ----
thread'main' panicked at 'Guess value must be greater than or equal to 1, got
200.', src/lib.rs:13:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected string
panic message: `"Guess value must be greater than or equal to 1, got
200."`,
expected substring: `"less than or equal to 100"`
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out;
finished in 0.00s
失败消息表明这个测试确实如我们所期望的那样导致了程序恐慌,但是恐慌消息没有包含预期的字符串 'Guess value must be less than or equal to 100'。在这种情况下我们得到的恐慌消息是 Guess value must be greater than or equal to 1, got 200。现在我们可以开始找出我们的错误在哪里了!
在测试中使用 Result<T, E>
到目前为止,我们的测试在失败时都会导致程序恐慌。我们也可以编写使用 Result<T, E> 的测试!这是清单 11 - 1 中的测试,重写后使用 Result<T, E> 并返回一个 Err 而不是导致程序恐慌:
文件名:src/lib.rs
#[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。
编写返回 Result<T, E> 的测试使你能够在测试体中使用问号运算符,这是一种编写测试的便捷方式,如果测试中的任何操作返回 Err 变体,测试就应该失败。
你不能在使用 Result<T, E> 的测试上使用 #[should_panic] 注解。要断言一个操作返回 Err 变体,不要 在 Result<T, E> 值上使用问号运算符。相反,使用 assert!(value.is_err())。
既然你已经知道了几种编写测试的方法,让我们看看当我们运行测试时会发生什么,并探索我们可以与 cargo test 一起使用的不同选项。
总结
恭喜你!你已经完成了“如何编写测试”实验。你可以在 LabEx 中练习更多实验来提升你的技能。