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