Rust 过程宏:自己动手写 #[derive]
每次你写下 #[derive(Debug)] 或 #[derive(Serialize)],编译器里都会运行一个小程序。它读一遍你的结构体,然后替你写好一个 impl 块。这个小程序就是过程宏(procedural macro),而且你也可以自己写。
可以把它想象成一位热心的办事员。你递过去一张表格(你的结构体),办事员把每一栏(字段)都看一遍,再把那些枯燥的文书工作填好还给你。在宏那一页,你认识了靠模式匹配工作的 macro_rules!。过程宏不一样:它就是普通的 Rust 函数,接收代码,再返回代码。这一页会一步一步带你做一个真正的过程宏,并给你看真实的运行结果。
过程宏的三种类型
Section titled “过程宏的三种类型”过程宏一共有三种。它们内部的工作原理相同,只是在使用者眼里长得不一样:
| 类型 | 使用者写什么 | 著名的例子 |
|---|---|---|
| derive 宏 | 写在结构体或枚举上的 #[derive(Serialize)] |
serde 的 Serialize、thiserror 的 Error、clap 的 Parser |
| 属性宏 | 写在函数等项上的 #[route(GET, "/")] |
#[tokio::main]、actix-web 的 #[get("/")] |
| 函数式宏 | sql!(SELECT * FROM users) |
sqlx 的 query!、yew 的 html! |
- derive 宏只会在你的类型旁边添加代码。你的结构体会原封不动地保留下来。
- 属性宏会替换它所标注的项。
#[tokio::main]接收你的async fn main,把它改写成一个启动 tokio 运行时的普通fn main。 - 函数式宏看起来像
macro_rules!的调用,但输入是什么意思,由一个 Rust 函数说了算。sqlx::query!能在编译时检查你的 SQL,靠的就是这一点。
其实你已经在用其他 crate 提供的 derive 宏了。下面两个 derive 宏正在干实事:Serialize 负责写生成 JSON 的代码(见 JSON 与 Serde),Error 根据 #[error(...)] 里的文字写出 Display 的代码(见 anyhow 与 thiserror):
use serde::Serialize;use thiserror::Error;
#[derive(Serialize)]struct Order { id: u32, item: String, quantity: u8,}
#[derive(Debug, Error)]enum ShopError { #[error("商品“{0}”已售罄")] OutOfStock(String),}
fn main() { let order = Order { id: 42, item: "茶壶".to_string(), quantity: 2 }; println!("{}", serde_json::to_string(&order).unwrap());
let err = ShopError::OutOfStock("茶壶".to_string()); println!("{err}");}输出:
{"id":42,"item":"茶壶","quantity":2}商品“茶壶”已售罄你一行 Display 实现、一行 JSON 代码都没写。这些代码是程序编译时由 derive 写出来的。
工作原理:输入 token,输出 token
Section titled “工作原理:输入 token,输出 token”编译器首先把你的源代码切成一个个 token(词法单元):名字、标点、字面量、括号。过程宏就是一个函数,它以 TokenStream 的形式拿到这些 token,再返回一个新的 TokenStream。接着编译器会编译你返回的内容,就好像这些代码是你亲手敲进去的一样。
下面是三种函数的样子。每种都要用一个属性标出来,编译器才知道它是哪一种:
use proc_macro::TokenStream;
// #[derive(HelloMacro)]:拿到结构体或枚举,返回要“添加”的代码。#[proc_macro_derive(HelloMacro)]pub fn hello_macro_derive(input: TokenStream) -> TokenStream { /* ... */ }
// #[route(GET, "/")]:拿到属性自己的 token 和被标注的项,返回“替换”后的代码。#[proc_macro_attribute]pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { /* ... */ }
// sql!(...):拿到括号里的全部内容,返回替换后的代码。#[proc_macro]pub fn sql(input: TokenStream) -> TokenStream { /* ... */ }原始的 TokenStream 只是一串 token,直接处理很费劲。所以几乎所有过程宏都会用到三个辅助 crate:
- syn 把 token 解析成语法树。你拿到的不再是 token,而是一个
DeriveInput,里面有类型的名字、泛型和字段。 - quote 把长得像 Rust 的代码变回 token。在
quote! { ... }里照常写 Rust,用#name就能把一个变量的值贴进去。 - proc-macro2 是对编译器
proc_macro类型的一层包装,syn 和 quote 都在用它。它在编译器之外也能工作,所以写辅助函数和测试会更方便。
有一点非常重要:你的宏在编译时运行。Cargo 会先为你自己的电脑编译宏所在的 crate,然后编译器在构建使用这个宏的 crate 时运行它。等程序真正运行起来,宏早就不见了,留下的只有它写出来的代码。
过程宏必须放在一个单独的 crate 里,而且这个 crate 要有特殊的 crate 类型。这种 crate 只能导出宏,不能导出普通函数或 trait。所以常见的做法是把两个 crate 并排放,就像 serde 和 serde_derive 那样:一个放 trait,一个放 derive。我们再加上第三个 crate:一个叫 shop 的小程序,用来使用前两个 crate。(想复习 crate 和工作空间?请看 Cargo 与 crate。)
mkdir procmacro-demo && cd procmacro-democargo new --lib hello_macrocargo new --lib hello_macro_derivecargo new shop完成后的文件结构是这样的:
procmacro-demo/├── Cargo.toml (工作空间)├── hello_macro/│ ├── Cargo.toml│ └── src/lib.rs (trait,以及重新导出的 derive)├── hello_macro_derive/│ ├── Cargo.toml (proc-macro = true)│ └── src/lib.rs (宏本身)└── shop/ ├── Cargo.toml └── src/main.rs (使用 #[derive(...)] 的程序)最外层的 Cargo.toml 把三个 crate 组成一个工作空间:
[workspace]members = ["hello_macro", "hello_macro_derive", "shop"]resolver = "2"只有宏所在的 crate 需要一个特殊设置:proc-macro = true。它还依赖 syn、quote 和 proc-macro2:
[package]name = "hello_macro_derive"version = "0.1.0"edition = "2024"
[lib]proc-macro = true
[dependencies]proc-macro2 = "1"quote = "1"syn = "2"另外两个 crate 都很普通。hello_macro 依赖 derive 所在的 crate,shop 只需要 hello_macro:
[dependencies]hello_macro_derive = { version = "0.1.0", path = "../hello_macro_derive" }[dependencies]hello_macro = { version = "0.1.0", path = "../hello_macro" }第 1 步:#[derive(HelloMacro)]
Section titled “第 1 步:#[derive(HelloMacro)]”先从很小的开始。我们希望任何加了 #[derive(HelloMacro)] 的类型都能得到一个 hello_macro() 函数,用来打印类型的名字。首先是 trait,写在 hello_macro/src/lib.rs 里。最后一行重新导出了 derive 宏,这样使用者只需要依赖这一个 crate。serde 也正是这么做的。
pub trait HelloMacro { fn hello_macro();}
pub trait Describe { fn type_name() -> &'static str; fn field_names() -> &'static [&'static str];}
// 重新导出 derive 宏,这样使用者只需要依赖这一个 crate。pub use hello_macro_derive::{Describe, HelloMacro};(Describe 留给第 2 步用。)接下来是宏本身,写在 hello_macro_derive/src/lib.rs 里:
use proc_macro::TokenStream;use quote::quote;use syn::{parse_macro_input, Data, DeriveInput, Fields}; // Data 和 Fields:第 2 步用
#[proc_macro_derive(HelloMacro)]pub fn hello_macro_derive(input: TokenStream) -> TokenStream { // 1. 把 token 解析成可以查看的语法树。 let ast = parse_macro_input!(input as DeriveInput); let name = &ast.ident;
// 2. 用 quote! 拼出新代码。 let generated = quote! { impl hello_macro::HelloMacro for #name { fn hello_macro() { println!("Hello, Macro! My name is {}!", stringify!(#name)); } } };
// 3. 把新的 token 交还给编译器。 generated.into()}三个步骤,几乎每个 derive 宏里都能看到:
parse_macro_input!把 token 变成一个DeriveInput。ast.ident是类型的名字,比如Product。如果解析失败,这个宏会替你返回一个像样的编译错误。quote!拼出impl块。#name把名字贴进去。stringify!(#name)把名字变成文本"Product"。.into()把 quote 的proc_macro2::TokenStream转换成编译器的proc_macro::TokenStream。
注意这里写的是完整路径 hello_macro::HelloMacro。生成的代码会被贴进使用者的文件里,所以不能指望使用者写了哪些 use。
当有人写下 #[derive(HelloMacro)] struct Product 时,编译器添加的代码和下面这个手写版本效果完全一样。这段代码不涉及宏 crate,所以能在 Playground 里运行(为了和示例项目的输出保持一致,字符串保留英文):
trait HelloMacro { fn hello_macro();}
struct Product;
// #[derive(HelloMacro)] 替你写的就是这段:impl HelloMacro for Product { fn hello_macro() { println!("Hello, Macro! My name is {}!", "Product"); }}
fn main() { Product::hello_macro();}输出:
Hello, Macro! My name is Product!手写一次没什么问题。可要是给 40 个类型都写一遍,derive 的价值就体现出来了。
第 2 步:用 #[derive(Describe)] 读取字段
Section titled “第 2 步:用 #[derive(Describe)] 读取字段”HelloMacro 只用到了类型的名字。真正的 derive,比如 serde 的,会去看字段。我们来做一个 Describe:它列出结构体的字段名,拿来做 CSV 表头很方便。syn 通过 ast.data 把字段交给你:
Data::Struct、Data::Enum或Data::Union:表示这是哪一种类型。- 如果是结构体,字段是
Fields::Named(struct Product { id: u32 })、Fields::Unnamed(struct Meters(f64))或Fields::Unit(struct Marker;)之一。
把下面的代码加到 hello_macro_derive/src/lib.rs 里第一个宏的下面:
#[proc_macro_derive(Describe)]pub fn describe_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); expand_describe(&ast) .unwrap_or_else(|err| err.to_compile_error()) .into()}
fn expand_describe(ast: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> { let name = &ast.ident;
let Data::Struct(data) = &ast.data else { return Err(syn::Error::new_spanned( name, "Describe can only be derived for structs", )); };
let field_names: Vec<String> = match &data.fields { Fields::Named(fields) => fields .named .iter() .map(|field| field.ident.as_ref().unwrap().to_string()) .collect(), Fields::Unit => Vec::new(), Fields::Unnamed(_) => { return Err(syn::Error::new_spanned( name, "Describe needs named fields, like `struct Point { x: i32 }`", )); } };
Ok(quote! { impl hello_macro::Describe for #name { fn type_name() -> &'static str { stringify!(#name) }
fn field_names() -> &'static [&'static str] { &[#(#field_names),*] } } })}新出现的几个要点:
- 真正的工作放在一个返回
syn::Result的普通辅助函数里。这样就能像其他错误处理代码一样使用?和return Err(...)。最上面的宏函数只负责把错误变成编译错误。 let ... else当场处理“不是结构体”的情况,让正常流程的代码保持平整、好读。#(#field_names),*是 quote 的重复语法。它把Vec里的每个字符串都贴进去,用逗号隔开,和macro_rules!里的$(...),*是一个思路。每个String都会变成一个字符串字面量,所以这里会变成&["id", "name", "price_cents"]。- 这里的
field.ident.as_ref().unwrap()是安全的:在Fields::Named里,每个字段都有名字。
现在在 shop/src/main.rs 里同时使用这两个 derive。注意 use hello_macro::{Describe, HelloMacro} 同时引入了 trait 和 derive 宏。它们名字相同,但处在不同的命名空间里,serde::Serialize 也正是这样工作的。
use hello_macro::{Describe, HelloMacro};
#[derive(HelloMacro, Describe)]struct Product { id: u32, name: String, price_cents: u64,}
#[derive(HelloMacro, Describe)]struct Customer { email: String, city: String,}
fn main() { Product::hello_macro(); Customer::hello_macro();
println!("{} has fields {:?}", Product::type_name(), Product::field_names());
// 一个 CSV 文件:表头来自 derive,数据行来自真实的值。 println!("{}", Customer::field_names().join(",")); println!("{},{}", ana.email, ana.city);
let teapot = Product { id: 7, name: "Teapot".to_string(), price_cents: 1999 }; println!("{} #{}: {} costs {} cents", Product::type_name(), teapot.id, teapot.name, teapot.price_cents);}在工作空间的文件夹里运行它:
cargo run -p shop输出(全新构建时):
Compiling proc-macro2 v1.0.107 Compiling unicode-ident v1.0.27 Compiling quote v1.0.47 Compiling syn v2.0.119 Compiling hello_macro_derive v0.1.0 (/home/you/procmacro-demo/hello_macro_derive) Compiling hello_macro v0.1.0 (/home/you/procmacro-demo/hello_macro) Compiling shop v0.1.0 (/home/you/procmacro-demo/shop) Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.45s Running `target/debug/shop`Hello, Macro! My name is Product!Hello, Macro! My name is Customer!Product has fields ["id", "name", "price_cents"]email,city[email protected],LisbonProduct #7: Teapot costs 1999 centsCompiling、Finished、Running 这几行是 Cargo 打印的,总是英文。留意一下顺序:Cargo 在 shop 之前先构建了 syn 和 hello_macro_derive,因为编译器在编译 shop 时需要运行你的宏。如果你给 Customer 加一个字段,下次构建时 CSV 表头会自动更新。
友好的错误信息
Section titled “友好的错误信息”如果有人把 #[derive(Describe)] 用在枚举上,会怎么样?因为 expand_describe 返回的是 syn::Error,使用者会看到一条和普通编译错误一模一样的报错。(这一页的错误示例都放在 shop/src/bin/broken.rs 里试过,所以报错里会出现这个路径。)
use hello_macro::Describe;
#[derive(Describe)]enum OrderStatus { Pending, Shipped,}error: Describe can only be derived for structs --> shop/src/bin/broken.rs:4:6 |4 | enum OrderStatus { | ^^^^^^^^^^^这条信息就是我们在宏里写的那句话(“Describe 只能用于结构体”)。这个报错好在两点:
syn::Error::new_spanned(name, ...)记住了名字在使用者文件里的位置(它的 span)。所以^^^^^指向的是OrderStatus,而不是你宏里的某一行。to_compile_error()把错误变成一个compile_error!("...")调用。然后编译器会像报告其他错误一样报告它。在普通代码里,你也可以用这个compile_error!宏。
元组结构体会得到另一条信息(“Describe 需要具名字段”),同样指向正确的位置:
error: Describe needs named fields, like `struct Point { x: i32 }` --> shop/src/bin/broken.rs:4:8 |4 | struct Meters(f64); | ^^^^^^写错误信息时,要站在使用宏的人的角度:说清楚哪里不对,以及怎样写才行。
用 cargo expand 查看生成的代码
Section titled “用 cargo expand 查看生成的代码”宏有时让人觉得像魔法,而魔法很难调试。cargo-expand 能让你看到所有宏运行之后的代码。安装一次就行(更多好用的工具见 Rust 工具与环境搭建):
cargo install cargo-expandcargo expand -p shop对我们的项目,它打印出的部分内容如下:
impl hello_macro::Describe for Product { fn type_name() -> &'static str { "Product" } fn field_names() -> &'static [&'static str] { &["id", "name", "price_cents"] }}impl hello_macro::Describe for Customer { fn type_name() -> &'static str { "Customer" } fn field_names() -> &'static [&'static str] { &["email", "city"] }}这就是你的 impl 块,字段名已经填好了,stringify!(#name) 也已经变成了 "Product"。当某个 derive 的行为和你预想的不一样时,先运行 cargo expand 看看。它也会展开 println! 等其他宏,所以要找你自己的那些 impl 块。
宏所在的 crate 只能导出宏,所以很难测试自己。简单的办法是:在一个使用这个宏的 crate 里写普通的测试。把下面的代码放进 hello_macro/tests/describe.rs,然后运行 cargo test -p hello_macro:
use hello_macro::Describe;
#[allow(dead_code)]#[derive(Describe)]struct Product { id: u32, name: String,}
#[test]fn lists_field_names_in_order() { assert_eq!(Product::type_name(), "Product"); assert_eq!(Product::field_names(), ["id", "name"]);}running 1 testtest lists_field_names_in_order ... ok那么,怎么测试用在枚举上时会给出友好的报错呢?普通测试做不到,因为测试文件本身就编译不过。这正是 trybuild crate 的用途。你把一些“应该编译失败”的小文件放进一个文件夹,trybuild 会逐个编译它们,再把编译器的报错和事先保存好的 .stderr 文件做比较:
#[test]fn ui() { let t = trybuild::TestCases::new(); t.compile_fail("tests/ui/*.rs");}大多数热门的宏 crate,包括 serde 和 thiserror,都是这样测试它们的错误信息的。
什么时候该写过程宏?
Section titled “什么时候该写过程宏?”过程宏很强大,但也有代价。动手之前,先诚实地问问自己:
- 编译时间。 syn 是个大 crate。每个用到你的宏的项目,第一次构建时都必须编译它。(写 derive 用 syn 的默认 feature 就够了。只有要解析函数体这类内容时,才需要
features = ["full"],而那会更慢。) - 复杂度。 你多了一个 crate、一堆“写代码的代码”,报错也更难追踪。
- 工具支持。 编辑器和 rust-analyzer 对普通代码的理解,比对宏生成的代码要好。
推荐按这个顺序尝试:
- 函数、带默认方法的 trait,或者泛型。大多数“重复代码”的问题到这一步就解决了。
macro_rules!宏(见宏)。它不需要额外的 crate。- 过程宏:当代码取决于类型的结构(它的名字、字段或变体),很多类型都需要它,而且别人也会用的时候。serde、thiserror 和 clap 用 derive 就是这个原因。
作为对比,下面是用 macro_rules! 实现的 Describe。它能用,也不需要额外的 crate,但结构体必须写在宏调用的里面。一旦你还想加文档注释、pub 字段或其他 derive,写起来很快就会变得别扭。
trait Describe { fn type_name() -> &'static str; fn field_names() -> &'static [&'static str];}
macro_rules! describe_struct { (struct $name:ident { $($field:ident : $ty:ty),* $(,)? }) => { struct $name { $($field: $ty),* }
impl Describe for $name { fn type_name() -> &'static str { stringify!($name) } fn field_names() -> &'static [&'static str] { &[$(stringify!($field)),*] } } };}
describe_struct!(struct Customer { email: String, city: String });
fn main() { println!("{} 的字段:{:?}", Customer::type_name(), Customer::field_names()); println!("{},{}", ana.email, ana.city);}输出:
Customer 的字段:["email", "city"][email protected],Lisbon如果只是给自己程序里的一两个类型用,这种写法(或者干脆手写 impl)往往是更好的选择。derive 对使用者更友好:他们照常写结构体,只需多加一行。
忘了写 proc-macro = true。 宏 crate 的 Cargo.toml 里如果没有 [lib] 这一节,这个 crate 就只是个普通库,编译器会拒绝编译:
error: the `#[proc_macro_derive]` attribute is only usable with crates of the `proc-macro` crate type --> hello_macro_derive/src/lib.rs:5:1 |5 | #[proc_macro_derive(HelloMacro)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0432]: unresolved import `proc_macro`用 panic! 或 unwrap() 处理使用者的错误。 这样“也能用”,但报错很含糊,指向的是整个 #[derive],而不是出问题的地方:
error: proc-macro derive panicked --> shop/src/bin/broken.rs:3:10 |3 | #[derive(Describe)] | ^^^^^^^^ | = help: message: Describe can only be derived for structs应该像第 2 步那样,用 new_spanned 返回一个 syn::Error。
生成的代码里用了短路径。 如果你写的是 impl Describe for #name,而不是 impl hello_macro::Describe for #name,那只有使用者碰巧导入了 Describe 时才能编译。有人写 #[derive(hello_macro::Describe)] 时就会得到这个报错:
error[E0405]: cannot find trait `Describe` in this scope --> shop/src/bin/broken.rs:1:10 |1 | #[derive(hello_macro::Describe)] | ^^^^^^^^^^^^^^^^^^^^^ not found in this scope | = note: this error originates in the derive macro `hello_macro::Describe`在 quote! 里一定要用完整路径:hello_macro::Describe、::std::fmt::Display、::core::option::Option。
这些练习要修改真实的项目,所以没有 Try it 按钮。请在自己的电脑上搭好本页的项目,然后运行 cargo run -p shop。
1. 数一数字段
Section titled “1. 数一数字段”给 Describe trait 和 derive 都加上第三个方法 fn field_count() -> usize。Product::field_count() 应该返回 3。提示:你已经有 field_names 这个 vector 了。在 quote! 之前把 field_names.len() 存进一个变量,再用 # 把它贴进去。
查看答案
在 hello_macro/src/lib.rs 里,给 trait 加上这个方法:
pub trait Describe { fn type_name() -> &'static str; fn field_names() -> &'static [&'static str]; fn field_count() -> usize;}在 expand_describe 里数出字段个数,并把这个方法加到生成的 impl 里:
let count = field_names.len();
Ok(quote! { impl hello_macro::Describe for #name { fn type_name() -> &'static str { stringify!(#name) }
fn field_names() -> &'static [&'static str] { &[#(#field_names),*] }
fn field_count() -> usize { #count } } })然后在 main 里写 println!("{} has {} fields", Product::type_name(), Product::field_count());,会打印:
Product has 3 fields计数发生在你的宏里,也就是编译时。cargo expand 会显示结果只是 fn field_count() -> usize { 3usize },一个常量,运行时没有任何额外的工作。
2. 支持泛型结构体
Section titled “2. 支持泛型结构体”试着把 #[derive(Describe)] 用在一个泛型结构体上,比如 struct Page<T> { items: Vec<T>, number: u32 }。它会报 error[E0107]: missing generics for struct Page,因为生成的代码写的是 impl hello_macro::Describe for Page,漏掉了 <T>。修改这个 derive,让泛型结构体也能用。提示:ast.generics.split_for_impl() 会给你三个部分,把它们贴进 impl 那一行。impl<T> 长什么样,可以回顾泛型。
查看答案
在 expand_describe 里拆分泛型,并在 impl 那一行用上全部三个部分:
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
Ok(quote! { impl #impl_generics hello_macro::Describe for #name #ty_generics #where_clause { fn type_name() -> &'static str { stringify!(#name) }
fn field_names() -> &'static [&'static str] { &[#(#field_names),*] } } })现在在 shop 里这样写就能运行了:
#[derive(Describe)]struct Page<T> { items: Vec<T>, number: u32,}
fn main() { let page = Page { items: vec!["a", "b"], number: 1 }; println!("{} {} {:?}", page.items.len(), page.number, Page::<String>::field_names());}2 1 ["items", "number"]impl_generics 是 <T>(带上所有约束),ty_generics 是 <T>,where_clause 是 where ... 那部分(如果有的话)。每个正经的 derive 都会这样做,所以记住 split_for_impl。
- 过程宏是一个在编译时运行的 Rust 函数:输入
TokenStream,输出TokenStream。 - 过程宏有三种:derive 宏(
#[derive(Serialize)])、属性宏(#[tokio::main])和函数式宏(sql!(...))。 - 它们放在单独的、设置了
proc-macro = true的 crate 里。通常由一个主 crate 把它们和 trait 一起重新导出。 - syn 负责解析输入(
DeriveInput、Data、Fields),quote 负责写出输出(#name、#(...),*)。 - 用
syn::Error::new_spanned(...).to_compile_error()报告使用者的错误,用cargo expand检查生成结果,用 trybuild 测试报错。 - 先试试函数、trait 或
macro_rules!。当很多类型都需要根据自身结构生成代码时,过程宏才值得写。
小测验
1. 过程宏的代码在什么时候运行?
2. #[tokio::main] 属于哪一种过程宏?
3. syn 和 quote 分别做什么?
4. 你的 derive 应该拒绝枚举。最好的做法是什么?
得分: 0 / 4
- 用于从属性生成代码的过程宏 — Rust 程序设计语言:官方教程,一步步实现
HelloMacroderive,还介绍了类属性宏和类函数宏。 - Procedural Macros — The Rust Reference(英文):三种过程宏、crate 类型、span 和卫生性(hygiene)的确切规则。
- syn 文档(英文):本页用到的
DeriveInput、Data、Fields、Error以及各种解析辅助工具。 - quote 文档(英文):
quote!里的#var插值和#(...),*重复是怎么工作的。 - proc-macro-workshop(英文):David Tolnay 免费的动手练习项目(builder derive、debug derive 等),配有一步步引导你的测试。