BurntSushi / quickcheck

Automated property based testing for Rust (with shrinking).
The Unlicense
2.31k stars 145 forks source link

How to combine quickcheck 1+ with fake? #320

Open marktani opened 11 months ago

marktani commented 11 months ago

Following the zero2production book With quickcheck = "0.9.2", quickcheck_macros = "0.9.1" and fake = "~2.3", the following works, because the function Arbitrary::arbitrary expected a Gen trait, and Gen used to be a trait:

#[derive(Debug, Clone)]
struct ValidEmailFixture(pub String);

impl quickcheck::Arbitrary for ValidEmailFixture {
    fn arbitrary<G: quickcheck::Gen>(g: &mut G) -> Self {
        let email = SafeEmail().fake_with_rng(g);
        Self(email)
    }
}

#[quickcheck_macros::quickcheck]
fn valid_emails_are_parsed_successfully(valid_email: ValidEmailFixture) -> bool {
    SubscriberEmail::parse(valid_email.0).is_ok()
}

Then we pass the mutable reference g into SafeEmail().fake_with_rng(g); and it works - fake generates random valid emails.

However, in quickcheck = "1", quickcheck::Gen is no trait anymore, only a struct. I haven't found out how to access the inner rng: rand::rngs::SmallRng of Gen to pass it into SafeEmail().fake_with_rng and get fake to work with quickcheck.

How is it still possible in 1+?

joshuamegnauth54 commented 7 months ago

I've encountered the same issue. I solved it in a hacky way by reimplementing SafeEmail.

#[cfg(test)]
mod tests {
    use fake::locales::{self, Data};
    use quickcheck::{Arbitrary, Gen};

    #[derive(Debug, Clone)]
    struct ValidEmailFixture(pub String);

    impl Arbitrary for ValidEmailFixture {
        fn arbitrary(g: &mut Gen) -> Self {
            let username = g
                .choose(locales::EN::NAME_FIRST_NAME)
                .unwrap()
                .to_lowercase();
            let domain = g.choose(&["com", "net", "org"]).unwrap();
            let email = format!("{username}@example.{domain}");
            Self(email)
        }
    }

    #[quickcheck_macros::quickcheck]
    fn valid_emails_are_parsed_successfully(email: ValidEmailFixture) -> bool {
        SubscriberEmail::parse(email.0).is_ok()
    }
}

Not ideal but also not too bad.

Zero to Production's author, Luca Palmieri, posted an excerpt that covers OP's code above if anyone wants to follow along.

npuichigo commented 6 months ago

Same issue here @BurntSushi. How to use quickcheck together with other tools like fake which needs an RNG

choco-green commented 2 months ago

I've encountered the same issue. I solved it in a hacky way by reimplementing SafeEmail.

#[cfg(test)]
mod tests {
    use fake::locales::{self, Data};
    use quickcheck::{Arbitrary, Gen};

    #[derive(Debug, Clone)]
    struct ValidEmailFixture(pub String);

    impl Arbitrary for ValidEmailFixture {
        fn arbitrary(g: &mut Gen) -> Self {
            let username = g
                .choose(locales::EN::NAME_FIRST_NAME)
                .unwrap()
                .to_lowercase();
            let domain = g.choose(&["com", "net", "org"]).unwrap();
            let email = format!("{username}@example.{domain}");
            Self(email)
        }
    }

    #[quickcheck_macros::quickcheck]
    fn valid_emails_are_parsed_successfully(email: ValidEmailFixture) -> bool {
        SubscriberEmail::parse(email.0).is_ok()
    }
}

Not ideal but also not too bad.

Zero to Production's author, Luca Palmieri, posted an excerpt that covers OP's code above if anyone wants to follow along.

An extension for more context if you came here from the same book - This is exactly how fake is implmenting the safeEmail function.

impl<L: Data + Copy> Dummy<SafeEmail<L>> for String {
    fn dummy_with_rng<R: Rng + ?Sized>(c: &SafeEmail<L>, rng: &mut R) -> Self {
        let username: String = FirstName(c.0).fake_with_rng::<&str, _>(rng).to_lowercase();
        let domain = ["com", "net", "org"].choose(rng).unwrap();
        format!("{}@example.{}", username, domain)
    }
}

For more information, there is #265 to read for why rng is no longer a dependency.

Enough Digging, Enjoy coding :3