Ignore PartialEq implementation for testing

Hello,

I have a struct with other structs in its fields, and I have implemented Debug, Display, and serde Serialize for it, and they all output a single line (string). I have also implemented PartialEq (and derived Eq) to compare this single line.

As you can guess, the serialized line of text doesn't include some additional information from the nested structs - that's ignored for Display, Debug and PartialEq. And that's OK - that's exactly how it should work.

The only time I don't want to ignore the additional information is testing. When I write a test, I want to compare the full struct of structs like I've derived PartialEq instead of implementing it. And ideally, I want to print an error like I derived Debug.

Is that possible?

Thank you.

Perhaps this is what you want:

$ cargo rr && cargo test && cat src/main.rs 
    Finished `release` profile [optimized] target(s) in 0.00s
     Running `target/release/mytest`
Got: true
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running unittests src/main.rs (target/debug/deps/mytest-c72684d14b35fd5f)

running 1 test
test test::test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

#[derive(Copy, Clone, Debug, Hash)]
#[cfg_attr(test, derive(PartialEq, Eq))]
struct Test(i32);
#[cfg(not(test))]
impl PartialEq for Test {
    fn eq(&self, _: &Self) -> bool {
        true
    }
}
#[cfg(not(test))]
impl Eq for Test {}
fn main() {
    println!("Got: {}", Test(1) == Test(2))
}
#[cfg(test)]
mod test {
    use super::*;
    #[test]
    fn test() {
        assert!(Test(1) != Test(2))
    }
}

It's generally a bad idea to make your code behave differently when being tested, because then your tests are not testing the behavior that non-test code will see. It's also impractical to do this for integration tests (cfg(test) only applies to code defined in the same crate as the test).

What I would suggest is having an “ordinary” struct that behaves in the default fashion, and a wrapper which behaves in the special one. Then you can convert from one to the other as you need, in tests or not.

#[derive(Debug, Eq, PartialEq, serde::Serialize)]
pub struct OrdinaryStruct {
    // ...many fields ...
}

pub struct OneLine(pub OrdinaryStruct);

impl fmt::Debug for OneLine {...}
impl fmt::Display for OneLine {...}
impl PartialEq for OneLine {...}
5 Likes

I'm normally much stricter in my tests than what is likely needed from a public API's perspective, so I understand wanting a stricter equivalence relation defined for tests. In fact I typically also inspect the exact errors returned from functions that return Result and not simply test that an error occurred since it's possible I "luck" out with such a test where an error occurred but for a different reason than intended which indicates a bug; however as alluded to above, you don't want to have functions behave differently in test code vs. non-test code since you prevent testing the actual code in non-test code. For this reason I define functions/types/traits that only exist in test code. For example a trait TestEq which is a much stricter form of equivalence relation than what Eq is—often this trait will represent true equality (i.e., identity of indiscernibles)—so long as TestEq::exact_equality => PartialEq::eq. Then my tests can test == the way it's defined but also a stricter equivalence relation as modeled by TestEq without worry of function collision.

I didn't realize I can do that, thank you.

That's a good idea. Thank you. I can call it FooBarRaw or FooBarData for the OrdinaryStruct and FooBar for the normal one.

Thank you. Could you please give me an example @philomathic_life ?

Essentially adapt @Neutron3529's example. Let's say I want to define a RationalNumber type. Rational numbers can be defined as the equivalence classes formed from the equivalence relation you were taught in grade school. Let's say I don't want to actually "reduce" the ratio of integers during construction though, but I do want to implement Eq such that it adheres to the equivalence relation that typically defines them.

use core::num::NonZeroI64;
#[derive(Clone, Copy, Debug)]
pub struct RationalNumber {
    numer: i64,
    denom: NonZeroI64,
}
impl PartialEq for RationalNumber {
    fn eq(&self, other: &Self) -> bool {
        self.numer.wrapping_mul(other.denom.get()) == self.denom.get().wrapping_mul(other.numer)
    }
}
impl Eq for RationalNumber {}
#[cfg(test)]
trait TrueEquality {
    fn equals(&self, other: &Self) -> bool;
}
#[cfg(test)]
impl TrueEquality for RationalNumber {
    fn equals(&self, other: &Self) -> bool {
        self.numer == other.numer && self.denom == other.denom
    }
}
#[cfg(test)]
mod tests {
    use super::{NonZeroI64, RationalNumber, TrueEquality as _};
    #[test]
    fn true_equality_implies_equivalence() {
        const TWO_FOURTHS: RationalNumber = RationalNumber {
            numer: 2,
            denom: NonZeroI64::new(4).unwrap(),
        };
        const LARGE_RATIONAL_EQUIVALENT_TO_ONE_HALF: RationalNumber = RationalNumber {
            numer: 0x3FFF_FFFF_FFFF_FFFF,
            denom: NonZeroI64::new(0x7FFF_FFFF_FFFF_FFFE).unwrap(),
        };
        const THREE_SEVENTHS: RationalNumber = RationalNumber {
            numer: 3,
            denom: NonZeroI64::new(7).unwrap(),
        };
        const EIGHT: RationalNumber = RationalNumber {
            numer: 8,
            denom: NonZeroI64::new(1).unwrap(),
        };
        const HUNDRED: i64 = 100;
        const HUNDRED_ONE: i64 = HUNDRED + 1;
        assert_eq!(
            TWO_FOURTHS, LARGE_RATIONAL_EQUIVALENT_TO_ONE_HALF,
            "bug in RationalNumber::eq"
        );
        assert_eq!(
            LARGE_RATIONAL_EQUIVALENT_TO_ONE_HALF, TWO_FOURTHS,
            "bug in RationalNumber::eq"
        );
        assert!(
            !TWO_FOURTHS.equals(&LARGE_RATIONAL_EQUIVALENT_TO_ONE_HALF),
            "bug in RationalNumber::equals"
        );
        assert!(
            !LARGE_RATIONAL_EQUIVALENT_TO_ONE_HALF.equals(&TWO_FOURTHS),
            "bug in RationalNumber::equals"
        );
        assert_ne!(THREE_SEVENTHS, EIGHT, "bug in RationalNumber::eq");
        assert_ne!(EIGHT, THREE_SEVENTHS, "bug in RationalNumber::eq");
        assert!(
            !THREE_SEVENTHS.equals(&EIGHT),
            "bug in RationalNumber::equals"
        );
        assert!(
            !EIGHT.equals(&THREE_SEVENTHS),
            "bug in RationalNumber::equals"
        );
        let mut rational;
        for numer in 0i64..=10_000 {
            for denom in 1i64..=10_000 {
                rational = RationalNumber {
                    numer,
                    denom: NonZeroI64::new(denom)
                        .unwrap_or_else(|| unreachable!("bug in core::num::NonZeroI64::new")),
                };
                assert!(rational.equals(&rational), "bug in RationalNumber::equals");
                assert_eq!(rational, rational, "bug in RationalNumber::eq");
            }
        }
        let mut equal_counter = 0u32;
        let mut rational2;
        for numer in 0i64..=HUNDRED {
            for denom in 1i64..=HUNDRED {
                rational = RationalNumber {
                    numer,
                    denom: NonZeroI64::new(denom)
                        .unwrap_or_else(|| unreachable!("bug in core::num::NonZeroI64::new")),
                };
                for numer2 in 0i64..=HUNDRED {
                    for denom2 in 1i64..=HUNDRED {
                        rational2 = RationalNumber {
                            numer: numer2,
                            denom: NonZeroI64::new(denom2).unwrap_or_else(|| {
                                unreachable!("bug in core::num::NonZeroI64::new")
                            }),
                        };
                        if rational.equals(&rational2) {
                            assert_eq!(rational, rational2, "bug in RationalNumber::eq");
                            // Can't overflow since `100 * 101 * 100 * 101 < u32::MAX`.
                            equal_counter += 1;
                        }
                    }
                }
            }
        }
        assert_eq!(HUNDRED * HUNDRED_ONE, equal_counter.into());
    }
}
Summary

Note this is a pathological example. In reality due to how easy it is for bugs to occur from users (including yourself) of your library to elevate equivalence to equality, one should try to strive to implement Eq like it is equality. This is not possible/feasible in a lot of situations though especially in a language like Rust that exposes things like pointers where one can often trivially do something that distinguishes between two entities that are normally in most situations supposed to be "equal". For something as trivial as RationalNumber, it will be used incorrectly even more often due to how something like 1/2 is "equal to" 2/4 is hardwired into your brain; thus I very likely would perform "reduction" in a single pub fn new.

This makes sense now. I assumed that TestEq is some special/hidden trait that works only when testing, but it's just a trait with a function with a more strict comparison. Thank you!

Ah, no. Also while PartialEq/Eq are nearly ubiquitously implemented; just like any trait impl, this does expose more API making breaking changes more likely. This may not be relevant for this specific example, but there are times where I only implement PartialEq/Eq for tests since it can sometimes be painful to write tests without such an impl. This isn't a problem since there is no conflicting impls; furthermore if you do decide to impl PartialEq/Eq for non-tests in the future, you don't have to worry about "accidentally" having different impls since as soon as you run your unit/integration tests, the compiler will complain about multiple impls forcing you to remove the impls for testing code or replacing your test impls with a private trait alternative in the case where the behavior is to be different.

I've just realized that it might be possible to write a custom assert_eq and then use custom::assert_eq it tests only and the custom assert_eq would use only the custom trait strict comparison.

That means that the tests will look normal and it will be even possible to switch to normal comparison by simply commenting the use.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.