2

I'm trying to make an assertions library for testing in Rust. Currently, I have statements like:

expect(value).to().be().equal_to(4);

It would be really nice to drop the parens on filler to and be functions to make it something like:

expect(value).to.be.equal_to(4);

I think this requires the to and be to be fields on struct returned by expect (Expectation). It currently looks like this:

struct Expectation<V: Debug> {
    value: V,
}

Is it possible to make it something like this:

struct Expectation<V: Debug> {
    value: V,
    to: Box<Expectation<V>>,
    be: Box<Expectation<V>>,
}

where to and be point to the struct they're in?

I've tried, but it's a tricky one to construct. I'm not even sure it is safe if the object is moved (maybe that can be prevented via Pin?).

I'm looking for any solution to allow the expect(value).to.be syntax above.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Callum Rogers
  • 15,630
  • 17
  • 67
  • 90
  • 2
    I'm kind of astonished that version with the filler is seen as more ergonomic or friendly than `expect(value).equal_to(4)` or even `expect_equal(value, 4)` – turbulencetoo Jan 15 '19 at 22:51
  • That's just one (hasty, small) example - not really here to debate the ergonomics of it (although I will say it does make a difference for larger assertions, especially with many composed together - see hamcrest or assertj for similar in Java). Also makes a nice brain teaser/is helping me learn rust. – Callum Rogers Jan 15 '19 at 23:03
  • 2
    I like the syntax, but *only in languages where it's normal*. Write code that goes with the aesthetic of the ecosystem, not just what you've used before. More pithily, **when in Rome, do as the Romans do**. – Shepmaster Jan 16 '19 at 01:47
  • See also: [Why can't I store a value and a reference to that value in the same struct?](https://stackoverflow.com/q/32300132/155423) – Shepmaster Jan 16 '19 at 01:49

3 Answers3

4

I'm looking for any solution to allow the expect(value).to.be syntax above.

Keep it simple then:

fn main() {
    expect(4).to.be.equal_to(3);
}

fn expect<T>(actual: T) -> To<T> {
    let be = Be {
        be: Expectation(actual),
    };
    To { to: be }
}

struct To<T> {
    pub to: Be<T>,
}

struct Be<T> {
    pub be: Expectation<T>,
}

struct Expectation<T>(T);

impl<T> Expectation<T> {
    fn equal_to<U>(&self, expected: U)
    where
        U: PartialEq<T>,
    {
        if expected != self.0 {
            panic!("Report error")
        }
    }
}

optionally want to skip [to and be]

use std::{ops::Deref, rc::Rc};

fn main() {
    expect(4).to.be.equal_to(3);
    expect(4).to.equal_to(3);
    expect(4).equal_to(3);
}

fn expect<T>(actual: T) -> Expectation<T> {
    let core = Core(Rc::new(actual));

    let be = Be { core: core.clone() };

    let to = To {
        be,
        core: core.clone(),
    };

    Expectation {
        to,
        core: core.clone(),
    }
}

struct Expectation<T> {
    pub to: To<T>,
    core: Core<T>,
}

impl<T> Deref for Expectation<T> {
    type Target = Core<T>;
    fn deref(&self) -> &Core<T> {
        &self.core
    }
}

struct To<T> {
    pub be: Be<T>,
    core: Core<T>,
}

impl<T> Deref for To<T> {
    type Target = Core<T>;
    fn deref(&self) -> &Core<T> {
        &self.core
    }
}

struct Be<T> {
    core: Core<T>,
}

impl<T> Deref for Be<T> {
    type Target = Core<T>;
    fn deref(&self) -> &Core<T> {
        &self.core
    }
}

struct Core<T>(Rc<T>);

impl<T> Clone for Core<T> {
    fn clone(&self) -> Self {
        Core(self.0.clone())
    }
}

impl<T> Core<T> {
    fn equal_to<U>(&self, expected: U)
    where
        U: PartialEq<T>,
    {
        if expected != *self.0 {
            panic!("Report error")
        }
    }
}

Some macros would reduce duplication, but I was too lazy to actually show that ;-)

When in Rome...

I would try to play to Rust's strengths when designing a test assertions library. To me, this means using traits to allow people to easily add custom assertions.

use crate::testlib::prelude::*;

fn main() {
    expect(4).to(be.equal_to(3));
    expect(4).to(equal_to(3));
}

mod testlib {
    // Shorthand variants that will always be imported.
    // Minimize what's in here to avoid name collisions
    pub mod prelude {
        use super::*;

        pub fn expect<A>(actual: A) -> Expectation<A> {
            Expectation::new(actual)
        }

        #[allow(non_upper_case_globals)]
        pub static be: Be = Be;

        pub fn equal_to<E>(expected: E) -> EqualTo<E> {
            EqualTo::new(expected)
        }

    }

    // All the meat of the implementation. Can be divided up nicely.

    pub trait Assertion<A> {
        fn assert(&self, actual: &A);
    }

    pub struct Expectation<A>(A);
    impl<A> Expectation<A> {
        pub fn new(actual: A) -> Self {
            Expectation(actual)
        }

        pub fn to(&self, a: impl Assertion<A>) {
            a.assert(&self.0)
        }
    }

    pub struct Be;

    impl Be {
        pub fn equal_to<E>(&self, expected: E) -> EqualTo<E> {
            EqualTo::new(expected)
        }
    }

    pub struct EqualTo<E>(E);

    impl<E> EqualTo<E> {
        pub fn new(expected: E) -> Self {
            EqualTo(expected)
        }
    }

    impl<A, E> Assertion<A> for EqualTo<E>
    where
        A: PartialEq<E>,
    {
        fn assert(&self, actual: &A) {
            if *actual != self.0 {
                panic!("report an error")
            }
        }
    }
}

Next steps I'd look into:

  • An assertion should probably report failures to some passed-in struct, not panic.
  • Add a to_not and/or not_to negative matcher.
  • Add composition of assertions.
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • Ah, this works nicely for the case where you always have to use `to` and `be` but not even you optionally want to skip them out. (which I realise I didn't state as a requirement above). Nice idea though! – Callum Rogers Jan 16 '19 at 09:56
2

I've had some success with lazily generating the to and be using the thunk crate:

struct Expectation<V: Debug> {
    value: Rc<V>,
    to: Thunk<Box<Expectation<V>>>,
    be: Thunk<Box<Expectation<V>>>,
}

fn expect<V: Debug>(value: V) -> Expectation<V> {
    expect_rc(Rc::new(value))
}

fn expect_rc<V: Debug>(value: Rc<V>) -> Expectation<V> {
    let to_cloned = value.clone();
    let be_cloned = value.clone();
    Expectation {
        value,
        to: Thunk::defer(|| Box::new(expect_rc(to_cloned))),
        be: Thunk::defer(|| Box::new(expect_rc(be_cloned))),
    }
}

impl<V: PartialEq + Debug> Expectation<V> {
    fn equals<R: Debug>(&self, expected: R)
    where
        V: PartialEq<R> + Clone,
    {
        assert_eq!(self.value.deref().clone(), expected);
    }
}

This works as I'd expect, thanks to magic of Deref + Deref conversion:

expect(4).to.be.equal_to(3);
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Callum Rogers
  • 15,630
  • 17
  • 67
  • 90
1

For designing a custom syntax, I'd just use a macro:

macro_rules! expect {
    ($subject:expr, to, $($attr:tt)*) => {
        expect!($subject, $($attr)*)
    };
    ($subject:expr, be, $($attr:tt)*) => {
        expect!($subject, $($attr)*)
    };
    ($subject:expr, equal_to $object:expr) => {
        assert_eq!($subject, $object)
    };
}

expect!(1, to, be, equal_to 1);

Deploying boxes and self-referential structs just to get a particular syntax is overkill.

Link to playground

notriddle
  • 640
  • 4
  • 10
  • Yeah, with macros you can do anything. I'm actually trying to avoid macros in this library as it's main raison d'etre in order to get good IDE autocompletion/support and better extensibility and composability. – Callum Rogers Jan 16 '19 at 09:58