Custom Matchers - mister-good-deal/rest GitHub Wiki
You can easily extend FluentTest with your own matchers:
// Define a custom matcher for your domain
trait UserMatchers<T> {
fn to_be_admin(self) -> Self;
}
// Implement it for the Assertion type
impl<T: AsRef<User> + Clone> UserMatchers<T> for Assertion<T> {
fn to_be_admin(self) -> Self {
let user = self.value.as_ref();
let success = user.role == Role::Admin;
// Create a sentence for the assertion
let sentence = AssertionSentence::new("be", "an admin");
// Add the step to the assertion chain and return it
return self.add_step(sentence, success);
}
}
// Use it in your tests
#[test]
fn test_user_permissions() {
// Enable enhanced output for more descriptive messages
config().enhanced_output(true).apply();
let admin_user = get_admin_user();
let regular_user = get_regular_user();
expect!(admin_user).to_be_admin(); // Passes
expect!(regular_user).not().to_be_admin(); // Passes
}
For more complex domains, you can create a whole set of related matchers:
// Domain-specific matcher traits
trait UserMatchers<T> {
fn to_be_admin(self) -> Self;
fn to_have_permission(self, permission: Permission) -> Self;
fn to_be_in_group(self, group: &str) -> Self;
}
// Implement for assertions
impl<T: AsRef<User> + Clone> UserMatchers<T> for Assertion<T> {
fn to_be_admin(self) -> Self {
let user = self.value.as_ref();
let success = user.role == Role::Admin;
let sentence = AssertionSentence::new("be", "an admin");
return self.add_step(sentence, success);
}
fn to_have_permission(self, permission: Permission) -> Self {
let user = self.value.as_ref();
let success = user.permissions.contains(&permission);
let sentence = AssertionSentence::new("have",
&format!("permission {:?}", permission));
return self.add_step(sentence, success);
}
fn to_be_in_group(self, group: &str) -> Self {
let user = self.value.as_ref();
let success = user.groups.contains(&group.to_string());
let sentence = AssertionSentence::new("be",
&format!("in group {}", group));
return self.add_step(sentence, success);
}
}
When creating custom matchers, follow these guidelines:
- Be descriptive: Use clear verb-object combinations in your assertions
-
Return self: Always return
self
to support chaining -
Use
AssertionSentence
: Create a sentence that reads naturally -
Support negation: Ensure your matcher works correctly with the
.not()
modifier -
Clone when needed: Use
.clone()
for values that need to be used after the assertion - Keep it focused: Each matcher should test one specific property
- Add documentation: Document your matchers with examples
Here's a complete example of a custom matcher for a blog post domain:
struct Post {
title: String,
content: String,
published: bool,
tags: Vec<String>,
author: User,
}
trait PostMatchers<T> {
fn to_be_published(self) -> Self;
fn to_have_tag(self, tag: &str) -> Self;
fn to_have_content_containing(self, text: &str) -> Self;
}
impl<T: AsRef<Post> + Clone> PostMatchers<T> for Assertion<T> {
fn to_be_published(self) -> Self {
let post = self.value.as_ref();
let success = post.published;
let sentence = AssertionSentence::new("be", "published");
return self.add_step(sentence, success);
}
fn to_have_tag(self, tag: &str) -> Self {
let post = self.value.as_ref();
let success = post.tags.contains(&tag.to_string());
let sentence = AssertionSentence::new("have", &format!("tag '{}'", tag));
return self.add_step(sentence, success);
}
fn to_have_content_containing(self, text: &str) -> Self {
let post = self.value.as_ref();
let success = post.content.contains(text);
let sentence = AssertionSentence::new("have",
&format!("content containing '{}'", text));
return self.add_step(sentence, success);
}
}
// Usage in tests
#[test]
fn test_blog_post() {
let post = get_blog_post();
expect!(post)
.to_be_published()
.and().to_have_tag("rust")
.and().to_have_content_containing("FluentTest");
}