|
| 1 | +### The `#[test]` attribute |
| 2 | +Today, rust programmers rely on a built in attribute called `#[test]`. |
| 3 | +All you have to do is mark a function as a test and include some asserts like so: |
| 4 | + |
| 5 | +```rust,ignore |
| 6 | +#[test] |
| 7 | +fn my_test() { |
| 8 | + assert!(2+2 == 4); |
| 9 | +} |
| 10 | +``` |
| 11 | + |
| 12 | +When this program is compiled using `rustc --test` or `cargo test`, it will |
| 13 | +produce an executable that can run this, and any other test function. This |
| 14 | +method of testing allows tests to live alongside code in an organic way. You |
| 15 | +can even put tests inside private modules: |
| 16 | + |
| 17 | +```rust,ignore |
| 18 | +mod my_priv_mod { |
| 19 | + fn my_priv_func() -> bool {} |
| 20 | +
|
| 21 | + #[test] |
| 22 | + fn test_priv_func() { |
| 23 | + assert!(my_priv_func()); |
| 24 | + } |
| 25 | +} |
| 26 | +``` |
| 27 | +Private items can thus be easily tested without worrying about how to expose |
| 28 | +the them to any sort of external testing apparatus. This is key to the |
| 29 | +ergonomics of testing in Rust. Semantically, however, it's rather odd. |
| 30 | +How does any sort of `main` function invoke these tests if they're not visible? |
| 31 | +What exactly is `rustc --test` doing? |
| 32 | + |
| 33 | +`#[test]` is implemented as a syntactic transformation inside the compiler's |
| 34 | +[`libsyntax` crate][libsyntax]. Essentially, it's a fancy macro, that |
| 35 | +rewrites the crate in 3 steps: |
| 36 | + |
| 37 | +#### Step 1: Re-Exporting |
| 38 | + |
| 39 | +As mentioned earlier, tests can exist inside private modules, so we need a way of |
| 40 | +exposing them to the main function, without breaking any existing code. To that end, |
| 41 | +`libsyntax` will create local modules called `__test_reexports` that recursively reexport tests. |
| 42 | +This expansion translates the above example into: |
| 43 | + |
| 44 | +```rust,ignore |
| 45 | +mod my_priv_mod { |
| 46 | + fn my_priv_func() -> bool {} |
| 47 | +
|
| 48 | + pub fn test_priv_func() { |
| 49 | + assert!(my_priv_func()); |
| 50 | + } |
| 51 | +
|
| 52 | + pub mod __test_reexports { |
| 53 | + pub use super::test_priv_func; |
| 54 | + } |
| 55 | +} |
| 56 | +``` |
| 57 | + |
| 58 | +Now, our test can be accessed as |
| 59 | +`my_priv_mod::__test_reexports::test_priv_func`. For deeper module |
| 60 | +structures, `__test_reexports` will reexport modules that contain tests, so a |
| 61 | +test at `a::b::my_test` becomes |
| 62 | +`a::__test_reexports::b::__test_reexports::my_test`. While this process seems |
| 63 | +pretty safe, what happens if there is an existing `__test_reexports` module? |
| 64 | +The answer: nothing. |
| 65 | + |
| 66 | +To explain, we need to understand [how the AST represents |
| 67 | +identifiers][Ident]. The name of every function, variable, module, etc. is |
| 68 | +not stored as a string, but rather as an opaque [Symbol][Symbol] which is |
| 69 | +essentially an ID number for each identifier. The compiler keeps a separate |
| 70 | +hashtable that allows us to recover the human-readable name of a Symbol when |
| 71 | +necessary (such as when printing a syntax error). When the compiler generates |
| 72 | +the `__test_reexports` module, it generates a new Symbol for the identifier, |
| 73 | +so while the compiler-generated `__test_reexports` may share a name with your |
| 74 | +hand-written one, it will not share a Symbol. This technique prevents name |
| 75 | +collision during code generation and is the foundation of Rust's macro |
| 76 | +hygiene. |
| 77 | + |
| 78 | +#### Step 2: Harness Generation |
| 79 | +Now that our tests are accessible from the root of our crate, we need to do something with them. |
| 80 | +`libsyntax` generates a module like so: |
| 81 | + |
| 82 | +```rust,ignore |
| 83 | +pub mod __test { |
| 84 | + extern crate test; |
| 85 | + const TESTS: &'static [self::test::TestDescAndFn] = &[/*...*/]; |
| 86 | +
|
| 87 | + #[main] |
| 88 | + pub fn main() { |
| 89 | + self::test::test_static_main(TESTS); |
| 90 | + } |
| 91 | +} |
| 92 | +``` |
| 93 | + |
| 94 | +While this transformation is simple, it gives us a lot of insight into how tests are actually run. |
| 95 | +The tests are aggregated into an array and passed to a test runner called `test_static_main`. |
| 96 | +We'll come back to exactly what `TestDescAndFn` is, but for now, the key takeaway is that there is a crate |
| 97 | +called [`test`][test] that is part of Rust core, that implements all of the runtime for testing. `test`'s interface is unstable, |
| 98 | +so the only stable way to interact with it is through the `#[test]` macro. |
| 99 | + |
| 100 | +#### Step 3: Test Object Generation |
| 101 | +If you've written tests in Rust before, you may be familiar with some of the optional attributes available on test functions. |
| 102 | +For example, a test can be annotated with `#[should_panic]` if we expect the test to cause a panic. It looks something like this: |
| 103 | + |
| 104 | +```rust,ignore |
| 105 | +#[test] |
| 106 | +#[should_panic] |
| 107 | +fn foo() { |
| 108 | + panic!("intentional"); |
| 109 | +} |
| 110 | +``` |
| 111 | + |
| 112 | +This means our tests are more than just simple functions, they have configuration information as well. `test` encodes this configuration |
| 113 | +data into a struct called [`TestDesc`][TestDesc]. For each test function in a crate, `libsyntax` will parse its attributes and generate a `TestDesc` instance. |
| 114 | +It then combines the `TestDesc` and test function into the predictably named `TestDescAndFn` struct, that `test_static_main` operates on. |
| 115 | +For a given test, the generated `TestDescAndFn` instance looks like so: |
| 116 | + |
| 117 | +```rust,ignore |
| 118 | +self::test::TestDescAndFn{ |
| 119 | + desc: self::test::TestDesc{ |
| 120 | + name: self::test::StaticTestName("foo"), |
| 121 | + ignore: false, |
| 122 | + should_panic: self::test::ShouldPanic::Yes, |
| 123 | + allow_fail: false, |
| 124 | + }, |
| 125 | + testfn: self::test::StaticTestFn(|| |
| 126 | + self::test::assert_test_result(::crate::__test_reexports::foo())), |
| 127 | +} |
| 128 | +``` |
| 129 | + |
| 130 | +Once we've constructed an array of these test objects, they're passed to the |
| 131 | +test runner via the harness generated in step 2. |
| 132 | + |
| 133 | +### Inspecting the generated code |
| 134 | +On nightly rust, there's an unstable flag called `unpretty` that you can use to print out the module source after macro expansion: |
| 135 | + |
| 136 | +```bash |
| 137 | +$ rustc my_mod.rs -Z unpretty=hir |
| 138 | +``` |
| 139 | + |
| 140 | +[test]: https://doc.rust-lang.org/test/index.html |
| 141 | +[TestDesc]: https://doc.rust-lang.org/test/struct.TestDesc.html |
| 142 | +[Symbol]: https://doc.rust-lang.org/nightly/nightly-rustc/syntax/ast/struct.Ident.html |
| 143 | +[Ident]: https://doc.rust-lang.org/nightly/nightly-rustc/syntax/ast/struct.Ident.html |
| 144 | +[eRFC]: https://github.com/rust-lang/rfcs/blob/master/text/2318-custom-test-frameworks.md |
| 145 | +[libsyntax]: https://github.com/rust-lang/rust/tree/master/src/libsyntax |
0 commit comments