Procedural Macros, pt. 5: Test Cases

We are going to test the code from our previous post with a couple of different approaches.

  1. synstructure::test_derive

synstructure::test_derive is a simple macro that evaluates whether our derive function generates the expected expansion for the macro’s invocation. It will form a part of our unit tests.

#[test]
fn test_getter_generation() {
  synstructure::test_derive! {
    derive_propertese {
      struct Person {
        name: String,
        age: u32
      }
    }
    expands to {
      impl Person {
        fn get_name(&self) -> &str {
          self.name.as_str()
        }

        fn get_age(&self) -> u32 {
          self.age
        }
      }
    }
    no_build
  } 
}

We use the no_build option to disable checking whether the code within the macro compiles. While the code we have provided in the example above is fairly self-contained and would compile, no_build can be useful in other more complicated scenarios.

  1. TryBuild

Unfortunately, we can’t use a procedural macro within the same crate that defines it. This means addition of unit tests that use our generated code would fail. Instead, we are going to use crate trybuild that provides a nice test harness for procedural macros. trybuild can be used to check if test cases fail compilation as well - a feature we are going to leverage later (when we add the ability to skip the generation of getters for a field).

Let’s start by adding trybuild to our crates dev dependencies.

cargo add --dev trybuild

Let’s now create a folder tests/ui inside our project’s root folder. Additionally, let’s create a file compile_tests.rs within. Edit compile_tests.rs to the following

#[test]
fn test_compile_errors() {
    let t = trybuild::TestCases::new();
    t.pass("tests/ui/test_getters.rs");
}

This code initializes the trybuild test harness. We then invoke it with a test file that we expect to pass compilation. In contrast, to test for failure, we’d invoke it with compile_fail. To complete the test case, we need to provide the content for tests_getters.rs (placed in tests/ui).

#[macro_use]
extern crate propertese;

#[derive(propertese)]
struct Person {
    name: String,
    age: u32
}

fn main() {
    let frank = Person {
        name: "Frank".to_owned(),
        age: 40
    };

    assert_eq!("Frank", frank.get_name());
    assert_eq!(40, frank.get_age());
}

With cargo test, our test harness will compile and run the code above - expecting it to pass without errors. In the final installment to this series, we will add the ability to skip the generation of a getter for a particular field - by the inclusion of attributes on a field.