Test Gen

Originally Published:

Tags: [ #Testing, #Rust ]


One of the primary goals of each unit test is to ensure that the test code is as decoupled from the source code as possible. Ideally, you want to be able to make a change to the production code and only have tests that fail that code change.

The Problem

Let's setup some code to show the problem. Given the following production code:

// cargo add uuid --features v4
use uuid::Uuid;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Role {
    Senator,
    Jedi,
    Sith,
}

#[derive(Debug, Clone)]
pub struct Character {
    pub id: Uuid,
    pub role: Role,
}

pub fn is_sith_lord(character: &Character) -> bool {
    character.role == Role::Sith
}

Let's say that we want to create few tests verifying the role of the character.

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn sith_character_is_sith() {
        let character = Character {
            id: Uuid::new_v4(),
            role: Role::Sith,
        };
        assert!(is_sith_lord(&character));
    }

    #[test]
    fn senator_character_is_not_sith() {
        let character = Character {
            id: Uuid::new_v4(),
            role: Role::Senator,
        };
        assert!(!is_sith_lord(&character));
    }
}

What happens when we want to add a few extra fields to our Character struct?

#[derive(Debug, Clone)]
pub struct Character {
    pub id: Uuid,
    pub firstname: String,
    pub lastname: String,
    pub role: Role,
}

Now, the compiler is complaining that the characters constructed in the tests don't have the fields.

cargo test
   Compiling playground v0.1.0 (/home/joel/git/devstopian/drafts/playground/rs)
error[E0063]: missing fields `firstname` and `lastname` in initializer of `Character`
  --> src/main.rs:28:25
   |
28 |         let character = Character {
   |                         ^^^^^^^^^ missing `firstname` and `lastname`

error[E0063]: missing fields `firstname` and `lastname` in initializer of `Character`
  --> src/main.rs:37:25
   |
37 |         let character = Character {
   |                         ^^^^^^^^^ missing `firstname` and `lastname`

For more information about this error, try `rustc --explain E0063`.

Each of the tests will need to be updated even though they have nothing to do with the new fields.

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn sith_character_is_sith() {
        let character = Character {
            id: Uuid::new_v4(),
            firstname: "Darth".to_string(),
            lastname: "Sidious".to_string(),
            role: Role::Sith,
        };
        assert!(is_sith_lord(&character));
    }

    #[test]
    fn senator_character_is_not_sith() {
        let character = Character {
            id: Uuid::new_v4(),
            firstname: "Padmé".to_string(),
            lastname: "Amidala".to_string(),
            role: Role::Senator,
        };
        assert!(!is_sith_lord(&character));
    }
}

The changes to the tests are accurate to the characters in the story, but they are not relevant to the function being tested. It is irrelevant to the is_sith_lord function whether you are testing a character named "Padmé", for example.

As the complexity grows in the codebase, it may become less apparent that particular datapoints in tests are irrelevant. The reader will need to reason about the purpose of the test and whether the string "Padmé" is relevant, for example.

Enter Gen

A better way to implement the tests is to put your constructed objects behind generators.

#[cfg(test)]
mod tests {
    use super::*;
    
    // Key Idea: Create a generator function.
    pub fn gen_character() -> Character {
        Character {
            id: Uuid::new_v4(),
            firstname: "sample_firstname".to_string(),
            lastname: "sample_lastname".to_string(),
            role: Role::Senator,
        }
    }

    #[test]
    fn sith_character_is_sith() {
        let character = Character {
            role = Role::Sith,
            ..gen_character()
        };
        assert!(is_sith_lord(&character));
    }

    #[test]
    fn senator_character_is_not_sith() {
        let character = Character {
            role = Role::Senator,
            ..gen_character()
        };
        assert!(!is_sith_lord(&character));
    }
}

Now, the gen_character function would be the only place needed to be updated or any test that directly relates to the changes. The complexity of the tests above is reduced ever so slightly, but you could imaging in a much larger codebase with 1000s of tests that this could be a huge reduction in complexity!

Update Log

Updated 2026-02

I upgraded the post from F# to Rust. F# was my primary language in 2019. I borrowed the idea of abstracting construction behind generators from the Haskell community and functional programming at large. The idea translates nicely into Rust with its spread syntax (e.g. ..gen_character()).