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// Disable colours during tests, otherwise trying to assert on the panic message becomes
158// significantly more annoying.
159#[cfg(not(test))]
160mod colours {
161    pub const TERM_RED: &str = "\x1B[31m";
162    pub const TERM_BOLD: &str = "\x1B[1m";
163    pub const TERM_RESET: &str = "\x1B[0m";
164}
165
166#[cfg(test)]
167mod colours {
168    pub const TERM_RED: &str = "";
169    pub const TERM_BOLD: &str = "";
170    pub const TERM_RESET: &str = "";
171}
172
173#[cfg(feature = "num")]
174extern crate num;
175
176/// This macro is no longer needed. Just use assert_that() function directly.
177#[macro_export]
178macro_rules! assert_that {
179    (&$subject:tt) => {
180        assert_that!($subject)
181    };
182    ($subject:tt) => {{
183        assert_that(&$subject)
184    }};
185    (&$subject:expr) => {
186        assert_that!($subject)
187    };
188    ($subject:expr) => {{
189        assert_that(&$subject)
190    }};
191}
192
193/// This macro is no longer needed. Just use asserting() function directly.
194#[macro_export]
195macro_rules! asserting {
196    (&$description:tt) => {
197        asserting!($description)
198    };
199    ($description:tt) => {{
200        asserting(&$description)
201    }};
202}
203
204pub trait DescriptiveSpec<'r> {
205    fn subject_name(&self) -> Option<&'r str>;
206    fn location(&self) -> Option<String>;
207    fn description(&self) -> Option<&'r str>;
208}
209
210/// A failed assertion.
211///
212/// This exposes builder methods to construct the final failure message.
213#[derive(Debug)]
214pub struct AssertionFailure<'r, T: 'r> {
215    spec: &'r T,
216    expected: Option<String>,
217    actual: Option<String>,
218}
219
220/// A description for an assertion.
221///
222/// This is created by the `asserting` function.
223#[derive(Debug)]
224pub struct SpecDescription<'r> {
225    value: &'r str,
226    location: Option<String>,
227}
228
229/// An assertion.
230///
231/// This is created by either the `assert_that` function, or by calling `that` on a
232/// `SpecDescription`.
233#[derive(Debug)]
234pub struct Spec<'s, S: 's> {
235    pub subject: &'s S,
236    pub subject_name: Option<&'s str>,
237    pub location: Option<String>,
238    pub description: Option<&'s str>,
239}
240
241/// Wraps a subject in a `Spec` to provide assertions against it.
242///
243/// The subject must be a reference.
244pub fn assert_that<S>(subject: &S) -> Spec<S> {
245    Spec {
246        subject,
247        subject_name: None,
248        location: None,
249        description: None,
250    }
251}
252
253/// Describes an assertion.
254pub fn asserting(description: &str) -> SpecDescription {
255    SpecDescription {
256        value: description,
257        location: None,
258    }
259}
260
261impl<'r> SpecDescription<'r> {
262    pub fn at_location(mut self, location: String) -> Self {
263        self.location = Some(location);
264        self
265    }
266
267    /// Creates a new assertion, passing through its description.
268    pub fn that<S>(self, subject: &'r S) -> Spec<'r, S> {
269        Spec {
270            subject,
271            subject_name: None,
272            location: self.location,
273            description: Some(self.value),
274        }
275    }
276}
277
278impl<'r, T> DescriptiveSpec<'r> for Spec<'r, T> {
279    fn subject_name(&self) -> Option<&'r str> {
280        self.subject_name
281    }
282
283    fn location(&self) -> Option<String> {
284        self.location.clone()
285    }
286
287    fn description(&self) -> Option<&'r str> {
288        self.description
289    }
290}
291
292impl<'r, T: DescriptiveSpec<'r>> AssertionFailure<'r, T> {
293    /// Construct a new AssertionFailure from a DescriptiveSpec.
294    pub fn from_spec(spec: &'r T) -> AssertionFailure<'r, T> {
295        AssertionFailure {
296            spec,
297            expected: None,
298            actual: None,
299        }
300    }
301
302    /// Builder method to add the expected value for the panic message.
303    pub fn with_expected(&mut self, expected: String) -> &mut Self {
304        self.expected = Some(expected);
305
306        self
307    }
308
309    /// Builder method to add the actual value for the panic message.
310    pub fn with_actual(&mut self, actual: String) -> &mut Self {
311        self.actual = Some(actual);
312
313        self
314    }
315
316    /// Builds the failure message with a description (if present), the expected value,
317    /// and the actual value and then calls `panic` with the created message.
318    #[track_caller]
319    pub fn fail(&mut self) {
320        assert!(
321            !(self.expected.is_none() || self.actual.is_none()),
322            "invalid assertion"
323        );
324
325        let location = self.maybe_build_location();
326        let subject_name = self.maybe_build_subject_name();
327        let description = self.maybe_build_description();
328
329        panic!(
330            "{}{}\n\t{}expected: {}\n\t but was: {}{}\n{}",
331            description,
332            subject_name,
333            TERM_RED,
334            self.expected.clone().unwrap(),
335            self.actual.clone().unwrap(),
336            TERM_RESET,
337            location
338        )
339    }
340
341    /// Calls `panic` with the provided message, prepending the assertion description
342    /// if present.
343    #[track_caller]
344    fn fail_with_message(&mut self, message: String) {
345        let location = self.maybe_build_location();
346        let subject_name = self.maybe_build_subject_name();
347        let description = self.maybe_build_description();
348
349        panic!(
350            "{}{}\n\t{}{}{}\n{}",
351            description, subject_name, TERM_RED, message, TERM_RESET, location
352        )
353    }
354
355    fn maybe_build_location(&self) -> String {
356        match self.spec.location() {
357            Some(value) => format!("\n\t{}at location: {}{}\n", TERM_BOLD, value, TERM_RESET),
358            None => "".to_string(),
359        }
360    }
361
362    fn maybe_build_description(&self) -> String {
363        match self.spec.description() {
364            Some(value) => format!("\n\t{}{}:{}", TERM_BOLD, value, TERM_RESET),
365            None => "".to_string(),
366        }
367    }
368
369    fn maybe_build_subject_name(&self) -> String {
370        match self.spec.subject_name() {
371            Some(value) => format!("\n\t{}for subject [{}]{}", TERM_BOLD, value, TERM_RESET),
372            None => "".to_string(),
373        }
374    }
375}
376
377impl<'s, S> Spec<'s, S> {
378    /// Provides the actual location of the assertion.
379    ///
380    /// Usually you would not call this directly, but use the macro forms of `assert_that` and
381    /// `asserting`, which will call this on your behalf with the correct location.
382    pub fn at_location(mut self, location: String) -> Self {
383        self.location = Some(location);
384
385        self
386    }
387
388    /// Associates a name with the subject.
389    ///
390    /// This will be displayed if the assertion fails.
391    pub fn named(mut self, subject_name: &'s str) -> Self {
392        self.subject_name = Some(subject_name);
393
394        self
395    }
396}
397
398impl<S> Spec<'_, S>
399where
400    S: Debug + PartialEq,
401{
402    /// Asserts that the actual value and the expected value are equal. The value type must
403    /// implement `PartialEq`.
404    ///
405    /// ```rust
406    /// # use speculoos::prelude::*;
407    /// assert_that(&"hello").is_equal_to(&"hello");
408    /// ```
409    #[track_caller]
410    pub fn is_equal_to<E: Borrow<S>>(&mut self, expected: E) {
411        let subject = self.subject;
412        let borrowed_expected = expected.borrow();
413
414        if !subject.eq(borrowed_expected) {
415            AssertionFailure::from_spec(self)
416                .with_expected(format!("<{:?}>", borrowed_expected))
417                .with_actual(format!("<{:?}>", subject))
418                .fail();
419        }
420    }
421
422    /// Asserts that the actual value and the expected value are not equal. The value type must
423    /// implement `PartialEq`.
424    ///
425    /// ```rust
426    /// # use speculoos::prelude::*;
427    /// assert_that(&"hello").is_not_equal_to(&"olleh");
428    /// ```
429    #[track_caller]
430    pub fn is_not_equal_to<E: Borrow<S>>(&mut self, expected: E) {
431        let subject = self.subject;
432        let borrowed_expected = expected.borrow();
433
434        if subject.eq(borrowed_expected) {
435            AssertionFailure::from_spec(self)
436                .with_expected(format!(
437                    "<{:?}> not equal to <{:?}>",
438                    subject, borrowed_expected
439                ))
440                .with_actual("equal".to_string())
441                .fail();
442        }
443    }
444}
445
446impl<'s, S> Spec<'s, S>
447where
448    S: Debug,
449{
450    /// Accepts a function accepting the value type which returns a bool. Returning false will
451    /// cause the assertion to fail.
452    ///
453    /// NOTE: The resultant panic message will only state the actual value. It's recommended that
454    /// you write your own assertion rather than relying upon this.
455    ///
456    /// `matches` returns &mut &Self, making it possible to chain multiple assertions.
457    ///
458    /// ```rust
459    /// # use speculoos::prelude::*;
460    /// assert_that(&"hello").matches(|x| x.eq(&"hello"));
461    /// ```
462    #[track_caller]
463    pub fn matches<F>(&mut self, matching_function: F) -> &mut Self
464    where
465        F: Fn(&'s S) -> bool,
466    {
467        let subject = self.subject;
468
469        if !matching_function(subject) {
470            AssertionFailure::from_spec(self)
471                .fail_with_message(format!("expectation failed for value <{:?}>", subject));
472        }
473
474        self
475    }
476
477    /// Transforms the subject of the `Spec` by passing it through to the provided mapping
478    /// function.
479    ///
480    /// ```rust
481    /// # use speculoos::prelude::*;
482    /// # #[derive(Debug, PartialEq)]
483    /// # struct TestStruct {
484    /// #     pub value: u8,
485    /// # }
486    /// let test_struct = TestStruct { value: 5 };
487    /// assert_that(&test_struct).map(|val| &val.value).is_equal_to(&5);
488    /// ```
489    #[track_caller]
490    pub fn map<F, T>(self, mapping_function: F) -> Spec<'s, T>
491    where
492        F: Fn(&'s S) -> &'s T,
493    {
494        Spec {
495            subject: mapping_function(self.subject),
496            subject_name: self.subject_name,
497            location: self.location.clone(),
498            description: self.description,
499        }
500    }
501}
502
503#[cfg(test)]
504mod tests {
505    #![allow(clippy::needless_borrows_for_generic_args)]
506    use super::prelude::*;
507
508    #[test]
509    fn should_be_able_to_use_macro_form_with_deliberate_reference() {
510        let test_vec = vec![1, 2, 3, 4, 5];
511
512        assert_that!(&test_vec).mapped_contains(|val| val * 2, &6);
513    }
514
515    #[test]
516    fn should_be_able_to_use_macro_form_without_deliberate_reference() {
517        let test_vec = vec![1, 2, 3, 4, 5];
518
519        assert_that!(test_vec).mapped_contains(|val| val * 2, &6);
520    }
521
522    #[test]
523    fn should_be_able_to_use_function_call_with_macro() {
524        struct Line {
525            x0: i32,
526            x1: i32,
527        }
528
529        impl Line {
530            fn get_delta_x(&self) -> i32 {
531                (self.x1 - self.x0).abs()
532            }
533        }
534
535        let line = Line { x0: 1, x1: 3 };
536        assert_that!(line.get_delta_x()).is_equal_to(2);
537        assert_that!(&line.get_delta_x()).is_equal_to(2);
538    }
539
540    #[test]
541    #[should_panic(expected = "\n\ttest condition:\n\texpected: <2>\n\t but was: <1>")]
542    fn should_contain_assertion_description_in_panic() {
543        asserting("test condition").that(&1).is_equal_to(&2);
544    }
545
546    #[test]
547    #[should_panic(expected = "\n\tclosure:\n\texpectation failed for value <\"Hello\">")]
548    fn should_contain_assertion_description_if_message_is_provided() {
549        let value = "Hello";
550        asserting("closure")
551            .that(&value)
552            .matches(|val| val.eq(&"Hi"));
553    }
554
555    #[test]
556    fn is_equal_to_should_support_multiple_borrow_forms() {
557        assert_that(&1).is_equal_to(1);
558        assert_that(&1).is_equal_to(&mut 1);
559        assert_that(&1).is_equal_to(&1);
560    }
561
562    #[test]
563    fn should_not_panic_on_equal_subjects() {
564        assert_that(&1).is_equal_to(&1);
565    }
566
567    #[test]
568    #[should_panic(expected = "\n\texpected: <2>\n\t but was: <1>")]
569    fn should_panic_on_unequal_subjects() {
570        assert_that(&1).is_equal_to(&2);
571    }
572
573    #[test]
574    fn is_not_equal_to_should_support_multiple_borrow_forms() {
575        assert_that(&1).is_not_equal_to(2);
576        assert_that(&1).is_not_equal_to(&mut 2);
577        assert_that(&1).is_not_equal_to(&2);
578    }
579
580    #[test]
581    fn should_not_panic_on_unequal_subjects_if_expected() {
582        assert_that(&1).is_not_equal_to(&2);
583    }
584
585    #[test]
586    #[should_panic(expected = "\n\texpected: <1> not equal to <1>\n\t but was: equal")]
587    fn should_panic_on_equal_subjects_if_expected_unequal() {
588        assert_that(&1).is_not_equal_to(&1);
589    }
590
591    #[test]
592    fn should_not_panic_if_value_matches() {
593        let value = "Hello";
594        assert_that(&value).matches(|val| val.eq(&"Hello"));
595    }
596
597    #[test]
598    #[should_panic(expected = "\n\texpectation failed for value <\"Hello\">")]
599    fn should_panic_if_value_does_not_match() {
600        let value = "Hello";
601        assert_that(&value).matches(|val| val.eq(&"Hi"));
602    }
603
604    #[test]
605    fn should_permit_chained_matches_calls() {
606        let value = ("Hello", "World");
607        assert_that(&value)
608            .matches(|val| val.0.eq("Hello"))
609            .matches(|val| val.1.eq("World"));
610    }
611
612    #[test]
613    fn should_be_able_to_map_to_inner_field_of_struct_when_matching() {
614        let test_struct = TestStruct { value: 5 };
615        assert_that(&test_struct)
616            .map(|val| &val.value)
617            .is_equal_to(&5);
618    }
619
620    #[derive(Debug, PartialEq)]
621    struct TestStruct {
622        pub value: u8,
623    }
624}