This post is part of an ongoing series about tests in Rust:
Equality for other types
In the possible improvements mentioned in the
last post, I mentioned
splitting NumberAssertions
into 2 functionalities:
-
comparing equality of 2 objects (
is_equal_to
,is_not_equal_to
) -
comparing order of 2 objects (
is_greater_than
,is_less_than
)
The funny thing is: we don’t need a new trait for these, they already exist and
are called PartialEq
et PartialOrd
.
Let’s focus on PartialEq
for the rest of this post.
What about chaining?
In the previous post, the API was allowing to chain multiple assertions. For example, this should be possible.
some_vec.has_length(3).contains("foo").contains("bar");
However, we do not have the NumberAssertions
trait anymore that can be pass
from assertion to the next one. So let’s create a struct
that will refer to
the tested value and might also contains more useful information that may be
printed out for test results. For now, let’s do it simple and just retain
information about the tested value.
struct Assertion<'v, V> {
value: &'v V,
}
Construct an Assertion
Let’s create a quick constructor for this new struct
and name it
assert_that
. This should allow us to write the following.
assert_that(1 + 1).is_equal_to(2);
assert_that
as a function
How about a simple function to start.
pub fn assert_that<V>(value: V) -> Assertion<'_, V> {
Assertion { value }
}
Oh wait, this consumes the value, we probably don’t want that. And it doesn’t work anyway.
error[E0106]: missing lifetime specifier
--> runit-assertions/src/lib.rs:46:46
|
46 | pub fn assert_that<V>(value: V) -> Assertion<'_, V> {
| ^^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value with an elided lifetime, but the lifetime cannot be derived from the arguments
help: consider using the `'static` lifetime
|
46 | pub fn assert_that<V>(value: V) -> Assertion<'static, V> {
| ~~~~~~~
For more information about this error, try `rustc --explain E0106`.
Let’s fix it…
pub fn assert_that<V>(value: &V) -> Assertion<'_, V> {
Assertion { value }
}
Argh, it doesn’t work either.
error[E0308]: mismatched types
--> src/lib.rs:44:13
|
5 | assert_that(1 + 1).is_equal_to(2);
| ^^^^^
| |
| expected reference, found integer
| help: consider borrowing here: `&(1 + 1)`
|
= note: expected reference `&_`
found type `{integer}`
Ok, we do not want to take ownership but typing &(1+1)
intead of 1+1
would make our API not easy to use. Can we do better? I believe we can thanks
to macros.
assert_that
as a macro
Let’s try it.
macro_rules! assert_that {
($value:expr) => {{
runit_assertions::Assertion { value: &($value) }
}};
}
Oh, but that wouldn’t work because value
is a private field. We could make
the field pub
but no reason to expose it only for the constructor. There is
actually another simple fix.
impl<'v, V> From<&'v V> for Assertion<'v, V>
where
V: 'v,
{
fn from(value: &'v V) -> Self {
Assertion { value }
}
}
macro_rules! assert_that {
($value:expr) => {{
runit_assertions::Assertion::from(&($value))
}};
}
Here we are, this finally works! Back to business after this little constructor’s digression.
Implementing is_equal_to
Let’s implement is_equal_to
for Assertion
now.
impl<'v, V> Assertion<'v, V> {
pub fn is_equal_to(&self, expected: V) -> &Self
where
V: PartialEq,
{
let t: &V = self.value;
assert!(t.eq(&expected));
self
}
}
The problem of that implementation? It doesn’t allow to do the following.
let s: String = some_function_under_test();
assert_that!(s).is_equal_to("foobar");
Indeed, "foobar"
is of type &str
but s
is of type String
. But I do
remember an API of std
that does exactly that: HashMap::get()
. For example,
the following does compile, even if the keys of the HashMap
are String
.
let mut map = HashMap::<String, usize>::default();
map.insert(String::from("foo"), 42);
let _ = map.get("foo");
This works thanks to the trait
Borrow
:
with it, you can have a String
and borrow it as if it was a &str
.
Will Borrow
help in our case? Well, yes and no. Ideally, I believe we should
be able to support all of the following combinations.
assert_that!("foo").is_equal_to("foo");
assert_that!(String::from("foo")).is_equal_to("foo");
assert_that!("foo").is_equal_to(String::from("foo"));
assert_that!(String::from("foo")).is_equal_to(String::from("foo"));
And I did try multiple different combinations using Borrow
, Into
, or
AsRef
… but none of them worked. Until I realized something. Will you find
the important part in the following fixed implementation?
pub fn is_equal_to<E>(&self, expected: E) -> &Self
where
V: PartialEq<E>,
{
let t: &V = self.value;
assert!(t.eq(&expected));
self
}
Of course you did spot that a new generic E
was introduced. This is mandatory
if we want to support a different type from V
. However, the important piece
is in the bounds of V
: it went from V: PartialEq
to V: PartialEq<E>
. Yes,
this single change will do the trick because String
does implement
PartialEq<String>
, PartialEq<&str>
, PartialEq<str>
and even
PartialEq<Cow<'a, str>>
.
Conclusion
In the end, we were able to implement something that allows us to write the following assertions and I find it pretty nice so far.
assert_that!(1 + 1).is_equal_to(2);
let foobar = String::from("foobar");
assert_that!(foobar).is_equal_to("foobar"); // String == &str
assert_that!("foobar").is_equal_to("foobar"); // &str == &str
assert_that!("foobar").is_equal_to(&foobar); // &str == &String
assert_that!("foobar").is_equal_to(foobar); // &str == String
Looks good, isn’t it? You can browse the result of this implementation in
woshilapin/runit
.