diff --git a/README.md b/README.md index ff7add8..6e1f551 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,8 @@ Easy-to-extend means that we can write assertions for custom types with minimal 1. use any predicate function as a custom assertion (see "[predicate as custom assertion]") 2. property-based assertions can be used with any type that implements the related property (see "[property-based assertions]") -3. write custom assertions by implementing two simple traits (see "[custom assertions]") +3. write custom assertion methods by defining and implementing an extension trait + (see "[custom assertions]") The mentioned references link to a chapter in the crate's documentation that describes the possibilities for custom assertions, including examples. diff --git a/src/lib.rs b/src/lib.rs index 089300f..042eae7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -415,12 +415,13 @@ //! //! # Custom assertions //! -//! `asserting` provides 4 ways to do custom assertions: +//! `asserting` provides 5 ways to do custom assertions: //! //! 1. Predicate functions as custom assertions used with the [`Spec::satisfies()`] method //! 2. Property base assertions for any type that implements a property trait //! 3. Custom expectations used with the [`Spec::expecting()`] method //! 4. Custom assertions methods +//! 5. Custom assertions without writing an expectation //! //! > 💡 //! > Often the easiest way to assert a custom type is to write a helper @@ -431,7 +432,15 @@ //! //! How to use predicate functions as custom assertions is described on the //! [`Spec::satisfies()`] method and in the [Examples](#predicate-as-custom-assertion) -//! chapter above. The other 3 ways are described in the following subchapters. +//! chapter above. The other 4 ways are described in the following subchapters. +//! +//! [`Expectation`]s enable us to write specialized assertions by combining +//! several basic expectations. In case a custom assertion cannot be composed +//! out of the provided expectations but writing a custom [`Expectation`] is too +//! cumbersome, we can write a custom assertion method directly without any +//! custom [`Expectation`]. See the +//! [Writing custom assertions without writing an expectation](#writing-custom-assertions-without-writing-an-expectation) +//! chapter below for an example. //! //! ## Property-based assertions //! @@ -687,6 +696,49 @@ //! assert_that!(subject).is_left(); //! ``` //! +//! ## Writing custom assertions without writing an expectation +//! +//! In real world projects custom assertions are often very specific, and custom +//! expectations will not be reusable anyway. Writing a custom assertion without +//! having to provide a custom [`Expectation`] is most likely the preferred way. +//! +//! Here is a simple assertion that checks whether a person is over 18 years +//! old: +//! +//! ``` +//! use asserting::prelude::*; +//! use asserting::spec::{FailingStrategy, Spec}; +//! +//! struct Person { +//! name: String, +//! age: u8, +//! } +//! +//! trait AssertOver18 { +//! fn is_over_18(self) -> Self; +//! } +//! +//! impl<'a, R> AssertOver18 for Spec<'a, Person, R> +//! where +//! R: FailingStrategy, +//! { +//! fn is_over_18(mut self) -> Self { +//! let actual = self.subject().age; +//! if actual < 18 { +//! let expression = self.expression(); +//! self.do_fail_with_message( +//! "expected {expression} to be over 18\n but was: {actual}\n expected: >= 18", +//! ); +//! } +//! self +//! } +//! } +//! +//! let person = Person { name: "Silvia".to_string(), age: 18 }; +//! +//! assert_that!(person).is_over_18(); +//! ``` +//! //! [`AssertElements`]: assertions::AssertElements //! [`AssertFailure`]: spec::AssertFailure //! [`Expectation`]: spec::Expectation diff --git a/src/spec/mod.rs b/src/spec/mod.rs index 9cd55b8..38b3b94 100644 --- a/src/spec/mod.rs +++ b/src/spec/mod.rs @@ -630,7 +630,7 @@ impl OwnedLocation { /// are collected in this struct. pub struct Spec<'a, S, R> { subject: S, - expression: Option>, + expression: Expression<'a>, description: Option>, location: Option>, failures: Vec, @@ -645,8 +645,8 @@ impl Spec<'_, S, R> { } /// Returns the expression (or subject name) if one has been set. - pub fn expression(&self) -> Option<&Expression<'_>> { - self.expression.as_ref() + pub fn expression(&self) -> &Expression<'_> { + &self.expression } /// Returns the location in source code or test code if it has been set. @@ -692,10 +692,10 @@ impl<'a, S, R> Spec<'a, S, R> { /// The diff format is set to "no highlighting". Failure messages will not /// highlight differences between the actual and the expected value. #[must_use = "a spec does nothing unless an assertion method is called"] - pub const fn new(subject: S, failing_strategy: R) -> Self { + pub fn new(subject: S, failing_strategy: R) -> Self { Self { subject, - expression: None, + expression: Expression::default(), description: None, location: None, failures: vec![], @@ -707,7 +707,7 @@ impl<'a, S, R> Spec<'a, S, R> { /// Sets the subject name or expression for this assertion. #[must_use = "a spec does nothing unless an assertion method is called"] pub fn named(mut self, subject_name: impl Into>) -> Self { - self.expression = Some(Expression(subject_name.into())); + self.expression = Expression(subject_name.into()); self } @@ -883,9 +883,8 @@ where #[track_caller] pub fn expecting(mut self, mut expectation: impl Expectation) -> Self { if !expectation.test(&self.subject) { - let default_expression = Expression::default(); - let expression = self.expression.as_ref().unwrap_or(&default_expression); - let message = expectation.message(expression, &self.subject, false, &self.diff_format); + let message = + expectation.message(&self.expression, &self.subject, false, &self.diff_format); self.do_fail_with_message(message); } self @@ -976,7 +975,7 @@ where /// Fails the assertion according the current failing strategy of this /// `Spec`. #[track_caller] - fn do_fail_with_message(&mut self, message: impl Into) { + pub fn do_fail_with_message(&mut self, message: impl Into) { let message = message.into(); let failure = AssertFailure { description: self.description.clone().map(String::from), @@ -1105,14 +1104,13 @@ impl<'a, I, R> Spec<'a, I, R> { I: IntoIterator, A: Fn(Spec<'a, T, CollectFailures>) -> Spec<'a, B, CollectFailures>, { - let default_expression = &Expression::default(); - let root_expression = self.expression.as_ref().unwrap_or(default_expression); + let root_expression = &self.expression; let mut position = -1; for item in self.subject { position += 1; let element_spec = Spec { subject: item, - expression: Some(format!("{root_expression} [{position}]").into()), + expression: format!("{root_expression} [{position}]").into(), description: None, location: self.location, failures: vec![], @@ -1185,15 +1183,14 @@ impl<'a, I, R> Spec<'a, I, R> { I: IntoIterator, A: Fn(Spec<'a, T, CollectFailures>) -> Spec<'a, B, CollectFailures>, { - let default_expression = &Expression::default(); - let root_expression = self.expression.as_ref().unwrap_or(default_expression); + let root_expression = &self.expression; let mut any_success = false; let mut position = -1; for item in self.subject { position += 1; let element_spec = Spec { subject: item, - expression: Some(format!("{root_expression} [{position}]").into()), + expression: format!("{root_expression} [{position}]").into(), description: None, location: self.location, failures: vec![],