speculoos/
lib.rs

1#![allow(clippy::wrong_self_convention)]
2
3//! Fluent test assertions in Rust
4//!
5//! Speculoos is a testing framework designed to make your assertions read like plain English.
6//! This allows you to more easily expose the intent of your test, rather than having it shrouded by
7//! assertions which work, but are opaque on their meaning.
8//!
9//! Methods available to assert with are dependent upon the type of the subject under test.
10//! Assertions are available for some basic types, but there is still a great deal missing from the
11//! standard library.
12//!
13//! ## Usage
14//!
15//! Add the dependency to your `Cargo.toml`:
16//!
17//! ```toml
18//! [dependencies]
19//! speculoos = "0.12.0"
20//! ```
21//!
22//! To quickly start using assertions, `use` the prelude module:
23//!
24//! ```rust
25//! use speculoos::prelude::*;
26//! ```
27//!
28//! ## Example
29//!
30//! We're going to make a few assertions on a `String` we create. Normally you would
31//! want to assert on the output of something, but we'll just pretend that something created it.
32//!
33//! First, we'll create a new test with our `String`.
34//!
35//! ```rust
36//! # #[warn(clippy::test_attr_in_doctest)]
37//! #[test]
38//! pub fn should_be_the_correct_string() {
39//!     let subject = "Hello World!";
40//! }
41//! ```
42//!
43//! Note that it is good practice to make sure that you name your test in a way that actually
44//! explains what it is trying to test. When you have a number of tests, and one of them fails,
45//! something like this is easier to understand:
46//!
47//! ```rust
48//! # #[warn(clippy::test_attr_in_doctest)]
49//! #[test]
50//! pub fn should_return_false_if_condition_does_not_hold() {
51//!     // ...
52//! }
53//! ```
54//!
55//! Rather than if you have a test like this:
56//!
57//! ```rust
58//! # #[warn(clippy::test_attr_in_doctest)]
59//! #[test]
60//! pub fn should_work() {
61//!     // ...
62//! }
63//! ```
64//!
65//! Unfortunately, our test isn't named very well at the moment, but given the lack of context,
66//! it'll have to do for now.
67//!
68//! Now that we have something to test, we need to actually start asserting on it. The first part
69//! to that is to provide it to the `assert_that` function. Note that we need to provide it as a
70//! reference.
71//!
72//! ```rust
73//! # use speculoos::prelude::*;
74//! #[test]
75//! pub fn should_be_the_correct_string() {
76//!     let subject = "Hello World!";
77//!     assert_that(&subject);
78//! }
79//! ```
80//!
81//! If we run that with `cargo test`, we'll see the following output:
82//!
83//! ```bash
84//! running 1 test
85//! test should_be_the_correct_string ... ok
86//! ```
87//!
88//! Our test compiles and passes, but we still haven't made any assertions. Let's make a simple one
89//! to start with. We'll check to see that it starts with the letter 'H'.
90//!
91//! ```rust
92//! # use speculoos::prelude::*;
93//! #[test]
94//! pub fn should_be_the_correct_string() {
95//!     let subject = "Hello World!";
96//!     assert_that(&subject).starts_with(&"H");
97//! }
98//! ```
99//!
100//! Once you run this, you'll notice that the test still passes. That's because we've just proven
101//! something that was already true. Usually you'll want to start with a failing test, and then
102//! change your code to make it pass, rather than writing the test after the implementation.
103//!
104//! But for the purpose of exploration, let's break the actual value. We'll change "Hello World!"
105//! to be "ello World!".
106//!
107//! ```rust
108//! # use speculoos::prelude::*;
109//! #[test]
110//! pub fn should_be_the_correct_string() {
111//!     let subject = "ello World!";
112//!     assert_that(&subject).starts_with(&"H");
113//! }
114//! ```
115//!
116//! This time, we see that the test fails, and we also get some output from our assertion to tell
117//! us what it was, and what it was expected to be:
118//!
119//! ```bash
120//! running 1 test
121//! test should_be_the_correct_string ... FAILED
122//!
123//! failures:
124//!
125//! ---- should_be_the_correct_string stdout ----
126//!     thread 'should_be_the_correct_string' panicked at src/lib.rs:204
127//!
128//!     expected string starting with <"H">
129//!      but was <"ello World!">'
130//! ```
131//!
132//! Great! So we've just encountered a failing test. This particular case is quite easy to fix up
133//! (just add the letter 'H' back to the start of the `String`), but we can also see that the panic
134//! message tells us enough information to work that out as well.
135//!
136//! Now, this was just a simple example, and there's a number of features not demonstrated, but
137//! hopefully it's enough to start you off with writing assertions in your tests using Speculoos.
138
139use std::borrow::Borrow;
140use std::cmp::PartialEq;
141use std::fmt::Debug;
142
143use colours::{TERM_BOLD, TERM_RED, TERM_RESET};
144
145pub mod boolean;
146pub mod hashmap;
147pub mod hashset;
148pub mod iter;
149pub mod numeric;
150pub mod option;
151pub mod path;
152pub mod prelude;
153pub mod result;
154pub mod string;
155pub mod vec;
156
157#[cfg(feature = "json")]
158pub mod json;
159
160// Disable colours during tests, otherwise trying to assert on the panic message becomes
161// significantly more annoying.
162#[cfg(not(test))]
163mod colours {
164    pub const TERM_RED: &str = "\x1B[31m";
165    pub const TERM_BOLD: &str = "\x1B[1m";
166    pub const TERM_RESET: &str = "\x1B[0m";
167}
168
169#[cfg(test)]
170mod colours {
171    pub const TERM_RED: &str = "";
172    pub const TERM_BOLD: &str = "";
173    pub const TERM_RESET: &str = "";
174}
175
176#[cfg(feature = "num")]
177extern crate num;
178
179/// This macro is no longer needed. Just use assert_that() function directly.
180#[macro_export]
181macro_rules! assert_that {
182    (&$subject:tt) => {
183        assert_that!($subject)
184    };
185    ($subject:tt) => {{
186        assert_that(&$subject)
187    }};
188    (&$subject:expr) => {
189        assert_that!($subject)
190    };
191    ($subject:expr) => {{
192        assert_that(&$subject)
193    }};
194}
195
196/// This macro is no longer needed. Just use asserting() function directly.
197#[macro_export]
198macro_rules! asserting {
199    (&$description:tt) => {
200        asserting!($description)
201    };
202    ($description:tt) => {{
203        asserting(&$description)
204    }};
205}
206
207pub trait DescriptiveSpec<'r> {
208    fn subject_name(&self) -> Option<&'r str>;
209    fn location(&self) -> Option<String>;
210    fn description(&self) -> Option<&'r str>;
211}
212
213/// A failed assertion.
214///
215/// This exposes builder methods to construct the final failure message.
216#[derive(Debug)]
217pub struct AssertionFailure<'r, T: 'r> {
218    spec: &'r T,
219    expected: Option<String>,
220    actual: Option<String>,
221}
222
223/// A description for an assertion.
224///
225/// This is created by the `asserting` function.
226#[derive(Debug)]
227pub struct SpecDescription<'r> {
228    value: &'r str,
229    location: Option<String>,
230}
231
232/// An assertion.
233///
234/// This is created by either the `assert_that` function, or by calling `that` on a
235/// `SpecDescription`.
236#[derive(Debug)]
237pub struct Spec<'s, S: 's> {
238    pub subject: &'s S,
239    pub subject_name: Option<&'s str>,
240    pub location: Option<String>,
241    pub description: Option<&'s str>,
242}
243
244/// Wraps a subject in a `Spec` to provide assertions against it.
245///
246/// The subject must be a reference.
247pub fn assert_that<S>(subject: &S) -> Spec<S> {
248    Spec {
249        subject,
250        subject_name: None,
251        location: None,
252        description: None,
253    }
254}
255
256/// Describes an assertion.
257pub fn asserting(description: &str) -> SpecDescription {
258    SpecDescription {
259        value: description,
260        location: None,
261    }
262}
263
264impl<'r> SpecDescription<'r> {
265    pub fn at_location(mut self, location: String) -> Self {
266        self.location = Some(location);
267        self
268    }
269
270    /// Creates a new assertion, passing through its description.
271    pub fn that<S>(self, subject: &'r S) -> Spec<'r, S> {
272        Spec {
273            subject,
274            subject_name: None,
275            location: self.location,
276            description: Some(self.value),
277        }
278    }
279}
280
281impl<'r, T> DescriptiveSpec<'r> for Spec<'r, T> {
282    fn subject_name(&self) -> Option<&'r str> {
283        self.subject_name
284    }
285
286    fn location(&self) -> Option<String> {
287        self.location.clone()
288    }
289
290    fn description(&self) -> Option<&'r str> {
291        self.description
292    }
293}
294
295impl<'r, T: DescriptiveSpec<'r>> AssertionFailure<'r, T> {
296    /// Construct a new AssertionFailure from a DescriptiveSpec.
297    pub fn from_spec(spec: &'r T) -> AssertionFailure<'r, T> {
298        AssertionFailure {
299            spec,
300            expected: None,
301            actual: None,
302        }
303    }
304
305    /// Builder method to add the expected value for the panic message.
306    pub fn with_expected(&mut self, expected: String) -> &mut Self {
307        self.expected = Some(expected);
308
309        self
310    }
311
312    /// Builder method to add the actual value for the panic message.
313    pub fn with_actual(&mut self, actual: String) -> &mut Self {
314        self.actual = Some(actual);
315
316        self
317    }
318
319    /// Builds the failure message with a description (if present), the expected value,
320    /// and the actual value and then calls `panic` with the created message.
321    #[track_caller]
322    pub fn fail(&mut self) {
323        assert!(
324            !(self.expected.is_none() || self.actual.is_none()),
325            "invalid assertion"
326        );
327
328        let location = self.maybe_build_location();
329        let subject_name = self.maybe_build_subject_name();
330        let description = self.maybe_build_description();
331
332        panic!(
333            "{}{}\n\t{}expected: {}\n\t but was: {}{}\n{}",
334            description,
335            subject_name,
336            TERM_RED,
337            self.expected.clone().unwrap(),
338            self.actual.clone().unwrap(),
339            TERM_RESET,
340            location
341        )
342    }
343
344    /// Calls `panic` with the provided message, prepending the assertion description
345    /// if present.
346    #[track_caller]
347    fn fail_with_message(&mut self, message: String) {
348        let location = self.maybe_build_location();
349        let subject_name = self.maybe_build_subject_name();
350        let description = self.maybe_build_description();
351
352        panic!(
353            "{}{}\n\t{}{}{}\n{}",
354            description, subject_name, TERM_RED, message, TERM_RESET, location
355        )
356    }
357
358    fn maybe_build_location(&self) -> String {
359        match self.spec.location() {
360            Some(value) => format!("\n\t{}at location: {}{}\n", TERM_BOLD, value, TERM_RESET),
361            None => "".to_string(),
362        }
363    }
364
365    fn maybe_build_description(&self) -> String {
366        match self.spec.description() {
367            Some(value) => format!("\n\t{}{}:{}", TERM_BOLD, value, TERM_RESET),
368            None => "".to_string(),
369        }
370    }
371
372    fn maybe_build_subject_name(&self) -> String {
373        match self.spec.subject_name() {
374            Some(value) => format!("\n\t{}for subject [{}]{}", TERM_BOLD, value, TERM_RESET),
375            None => "".to_string(),
376        }
377    }
378}
379
380impl<'s, S> Spec<'s, S> {
381    /// Provides the actual location of the assertion.
382    ///
383    /// Usually you would not call this directly, but use the macro forms of `assert_that` and
384    /// `asserting`, which will call this on your behalf with the correct location.
385    pub fn at_location(mut self, location: String) -> Self {
386        self.location = Some(location);
387
388        self
389    }
390
391    /// Associates a name with the subject.
392    ///
393    /// This will be displayed if the assertion fails.
394    pub fn named(mut self, subject_name: &'s str) -> Self {
395        self.subject_name = Some(subject_name);
396
397        self
398    }
399}
400
401impl<S> Spec<'_, S>
402where
403    S: Debug + PartialEq,
404{
405    /// Asserts that the actual value and the expected value are equal. The value type must
406    /// implement `PartialEq`.
407    ///
408    /// ```rust
409    /// # use speculoos::prelude::*;
410    /// assert_that(&"hello").is_equal_to(&"hello");
411    /// ```
412    #[track_caller]
413    pub fn is_equal_to<E: Borrow<S>>(&mut self, expected: E) {
414        let subject = self.subject;
415        let borrowed_expected = expected.borrow();
416
417        if !subject.eq(borrowed_expected) {
418            AssertionFailure::from_spec(self)
419                .with_expected(format!("<{:?}>", borrowed_expected))
420                .with_actual(format!("<{:?}>", subject))
421                .fail();
422        }
423    }
424
425    /// Asserts that the actual value and the expected value are not equal. The value type must
426    /// implement `PartialEq`.
427    ///
428    /// ```rust
429    /// # use speculoos::prelude::*;
430    /// assert_that(&"hello").is_not_equal_to(&"olleh");
431    /// ```
432    #[track_caller]
433    pub fn is_not_equal_to<E: Borrow<S>>(&mut self, expected: E) {
434        let subject = self.subject;
435        let borrowed_expected = expected.borrow();
436
437        if subject.eq(borrowed_expected) {
438            AssertionFailure::from_spec(self)
439                .with_expected(format!(
440                    "<{:?}> not equal to <{:?}>",
441                    subject, borrowed_expected
442                ))
443                .with_actual("equal".to_string())
444                .fail();
445        }
446    }
447}
448
449impl<'s, S> Spec<'s, S>
450where
451    S: Debug,
452{
453    /// Accepts a function accepting the value type which returns a bool. Returning false will
454    /// cause the assertion to fail.
455    ///
456    /// NOTE: The resultant panic message will only state the actual value. It's recommended that
457    /// you write your own assertion rather than relying upon this.
458    ///
459    /// `matches` returns &mut &Self, making it possible to chain multiple assertions.
460    ///
461    /// ```rust
462    /// # use speculoos::prelude::*;
463    /// assert_that(&"hello").matches(|x| x.eq(&"hello"));
464    /// ```
465    #[track_caller]
466    pub fn matches<F>(&mut self, matching_function: F) -> &mut Self
467    where
468        F: Fn(&'s S) -> bool,
469    {
470        let subject = self.subject;
471
472        if !matching_function(subject) {
473            AssertionFailure::from_spec(self)
474                .fail_with_message(format!("expectation failed for value <{:?}>", subject));
475        }
476
477        self
478    }
479
480    /// Transforms the subject of the `Spec` by passing it through to the provided mapping
481    /// function.
482    ///
483    /// ```rust
484    /// # use speculoos::prelude::*;
485    /// # #[derive(Debug, PartialEq)]
486    /// # struct TestStruct {
487    /// #     pub value: u8,
488    /// # }
489    /// let test_struct = TestStruct { value: 5 };
490    /// assert_that(&test_struct).map(|val| &val.value).is_equal_to(&5);
491    /// ```
492    #[track_caller]
493    pub fn map<F, T>(self, mapping_function: F) -> Spec<'s, T>
494    where
495        F: Fn(&'s S) -> &'s T,
496    {
497        Spec {
498            subject: mapping_function(self.subject),
499            subject_name: self.subject_name,
500            location: self.location.clone(),
501            description: self.description,
502        }
503    }
504}
505
506#[cfg(test)]
507mod tests {
508    #![allow(clippy::needless_borrows_for_generic_args)]
509    use super::prelude::*;
510
511    #[test]
512    fn should_be_able_to_use_macro_form_with_deliberate_reference() {
513        let test_vec = vec![1, 2, 3, 4, 5];
514
515        assert_that!(&test_vec).mapped_contains(|val| val * 2, &6);
516    }
517
518    #[test]
519    fn should_be_able_to_use_macro_form_without_deliberate_reference() {
520        let test_vec = vec![1, 2, 3, 4, 5];
521
522        assert_that!(test_vec).mapped_contains(|val| val * 2, &6);
523    }
524
525    #[test]
526    fn should_be_able_to_use_function_call_with_macro() {
527        struct Line {
528            x0: i32,
529            x1: i32,
530        }
531
532        impl Line {
533            fn get_delta_x(&self) -> i32 {
534                (self.x1 - self.x0).abs()
535            }
536        }
537
538        let line = Line { x0: 1, x1: 3 };
539        assert_that!(line.get_delta_x()).is_equal_to(2);
540        assert_that!(&line.get_delta_x()).is_equal_to(2);
541    }
542
543    #[test]
544    #[should_panic(expected = "\n\ttest condition:\n\texpected: <2>\n\t but was: <1>")]
545    fn should_contain_assertion_description_in_panic() {
546        asserting("test condition").that(&1).is_equal_to(&2);
547    }
548
549    #[test]
550    #[should_panic(expected = "\n\tclosure:\n\texpectation failed for value <\"Hello\">")]
551    fn should_contain_assertion_description_if_message_is_provided() {
552        let value = "Hello";
553        asserting("closure")
554            .that(&value)
555            .matches(|val| val.eq(&"Hi"));
556    }
557
558    #[test]
559    fn is_equal_to_should_support_multiple_borrow_forms() {
560        assert_that(&1).is_equal_to(1);
561        assert_that(&1).is_equal_to(&mut 1);
562        assert_that(&1).is_equal_to(&1);
563    }
564
565    #[test]
566    fn should_not_panic_on_equal_subjects() {
567        assert_that(&1).is_equal_to(&1);
568    }
569
570    #[test]
571    #[should_panic(expected = "\n\texpected: <2>\n\t but was: <1>")]
572    fn should_panic_on_unequal_subjects() {
573        assert_that(&1).is_equal_to(&2);
574    }
575
576    #[test]
577    fn is_not_equal_to_should_support_multiple_borrow_forms() {
578        assert_that(&1).is_not_equal_to(2);
579        assert_that(&1).is_not_equal_to(&mut 2);
580        assert_that(&1).is_not_equal_to(&2);
581    }
582
583    #[test]
584    fn should_not_panic_on_unequal_subjects_if_expected() {
585        assert_that(&1).is_not_equal_to(&2);
586    }
587
588    #[test]
589    #[should_panic(expected = "\n\texpected: <1> not equal to <1>\n\t but was: equal")]
590    fn should_panic_on_equal_subjects_if_expected_unequal() {
591        assert_that(&1).is_not_equal_to(&1);
592    }
593
594    #[test]
595    fn should_not_panic_if_value_matches() {
596        let value = "Hello";
597        assert_that(&value).matches(|val| val.eq(&"Hello"));
598    }
599
600    #[test]
601    #[should_panic(expected = "\n\texpectation failed for value <\"Hello\">")]
602    fn should_panic_if_value_does_not_match() {
603        let value = "Hello";
604        assert_that(&value).matches(|val| val.eq(&"Hi"));
605    }
606
607    #[test]
608    fn should_permit_chained_matches_calls() {
609        let value = ("Hello", "World");
610        assert_that(&value)
611            .matches(|val| val.0.eq("Hello"))
612            .matches(|val| val.1.eq("World"));
613    }
614
615    #[test]
616    fn should_be_able_to_map_to_inner_field_of_struct_when_matching() {
617        let test_struct = TestStruct { value: 5 };
618        assert_that(&test_struct)
619            .map(|val| &val.value)
620            .is_equal_to(&5);
621    }
622
623    #[derive(Debug, PartialEq)]
624    struct TestStruct {
625        pub value: u8,
626    }
627}