specta icon indicating copy to clipboard operation
specta copied to clipboard

Snapshot testing

Open oscartbeaumont opened this issue 11 months ago • 4 comments

I think we should be testing the export feature via exporting not inling. We should copy trybuild and allow easily updating all the tests in one go to make maintaince easier.

oscartbeaumont avatar Feb 13 '25 07:02 oscartbeaumont

@oscartbeaumont I built https://crates.io/crates/tryexpand (a heavily modified/improved fork of macrotest) specifically for scenarios like these.

While it is tailored around snapshot-testing proc-macro expansion it also supports snapshotting the runtime stdout/stderr output of the binary you're testing (and quite a bit more, if desired), all in one go and from one suite of test projects.

You would add test projects that might look something like this:

// tests/tryexpand/pass/example.rs

use specta::{Type, TypeCollection};
use specta_typescript::Typescript;

#[derive(Type)]
pub struct TypeOne {
    pub a: String,
    pub b: GenericType<i32>,
    #[serde(rename = "cccccc")]
    pub c: MyEnum,
}

#[derive(Type)]
pub struct GenericType<A> {
    pub my_field: String,
    pub generic: A,
}

#[derive(Type)]
pub enum MyEnum {
    A,
    B,
    C,
}

fn main() {
    let types = TypeCollection::default().register::<TypeOne>();
    let exported = Typescript::default().export(&types).unwrap();
    println!("{exported}");
} 
// tests/tryexpand/fail/missing_type_derive.rs

use specta::{TypeCollection};
use specta_typescript::Typescript;

pub struct TypeOne {}

fn main() {
    let types = TypeCollection::default().register::<TypeOne>();
    let exported = Typescript::default().export(&types).unwrap();
    println!("{exported}");
} 

… and then run snapshot tests over them like this:

// tests/tryexpand.rs

#[test]
pub fn pass() {
    tryexpand::run(
        ["tests/tryexpand/pass/*.rs"]
    ).expect_pass();
}

#[test]
pub fn fail() {
    tryexpand::expand(
        ["tests/tryexpand/fail/*.rs"]
    ).expect_fail();
}

The use of tryexpand::run(…) makes tryexpand only snapshot the runtime output on stdout/stderr. If you also want to snapshot the macro-expansion (as is the case with fn fail(), above) you would use tryexpand::expand(…).and_run() instead.

There are a bunch of options that can be combined to fit your requirements:

  • tryexpand::expand(…)
  • tryexpand::check(…)
  • tryexpand::run(…)
  • tryexpand::run_tests(…)
  • .and_check(…)
  • .and_run(…)
  • .and_run_tests(…)

You can run the tests with TRYEXPAND=overwrite to auto-accept and overwrite any new or changed snapshot:

$ TRYEXPAND=overwrite cargo test

I'd happily help setting up a tryexpand-based test suite, if there's interest for it.

regexident avatar Jul 25 '25 08:07 regexident

That's really cool, thanks for sharing. Will look into using it!

oscartbeaumont avatar Jul 30 '25 13:07 oscartbeaumont

I'm using tryexpand extensively for testing the derive macros of my enumcapsulate crate, in case you'd like so see some real-world usage.

Like I said I'd happily open a PR with basic tests already set up for you, if that's something that sounds useful to you.

regexident avatar Jul 30 '25 14:07 regexident

That would be awesome if you have the time but also no stress if not!

oscartbeaumont avatar Aug 03 '25 15:08 oscartbeaumont