output_tracker/
lib.rs

1//! A utility for writing state-based tests using [nullables] instead of mocks.
2//! It can track the state of dependencies which can then be asserted in a test.
3//!
4//! In architectural patterns like Ports & Adapters or Hexagonal Architecture
5//! code that interacts with the outside world is encapsulated from the domain
6//! logic in some adapter or connector. An adapter or connector may implement
7//! an interface (or trait) that is interchangeable for different infrastructure
8//! or APIs of some third-party component or service.
9//!
10//! The calling code should not know which implementation is currently used. The
11//! instance of an adapter or connector to be used in a certain situation is
12//! injected into the calling service (inversion of control). Adapters and
13//! connectors are therefore also called dependencies.
14//!
15//! To test our code we have to set up all dependencies. Setting up the
16//! dependencies might be complex and running the tests needs some
17//! infrastructure to be set up as well. Often running such tests is slow and
18//! the dependencies must be configured separately for different test
19//! environments.
20//!
21//! [Nullables] are a pattern to test as much as possible of our code without
22//! actually using the infrastructure. Therefore, testing with nullables is
23//! easy to set up and the tests are running fast like unit tests.
24//!
25//! ## How does it work?
26//!
27//! We have two main structs, the
28//! [`OutputTracker`][non_threadsafe::OutputTracker] and the
29//! [`OutputSubject`][non_threadsafe::OutputSubject].
30//!
31//! An [`OutputTracker`][non_threadsafe::OutputTracker] can track any state of
32//! some component or any actions executed by the component.
33//! [`OutputTracker`][non_threadsafe::OutputTracker]s can only be created by
34//! calling the function [`create_tracker()`][non_threadsafe::OutputSubject::create_tracker]
35//! of an [`OutputSubject`][non_threadsafe::OutputSubject].
36//!
37//! The [`OutputSubject`][non_threadsafe::OutputSubject] holds all
38//! [`OutputTracker`][non_threadsafe::OutputTracker] created through its
39//! [`create_tracker()`][non_threadsafe::OutputSubject::create_tracker]
40//! function. We can emit state or action data to all active
41//! [`OutputTracker`][non_threadsafe::OutputTracker]s by calling the function
42//! [`emit(data)`][non_threadsafe::OutputSubject::emit] on the
43//! [`OutputSubject`][non_threadsafe::OutputSubject].
44//!
45//! To read and assert the state or action data collected by an
46//! [`OutputTracker`][non_threadsafe::OutputTracker] we call the
47//! [`output()`][non_threadsafe::OutputTracker::output] function on the
48//! [`OutputTracker`][non_threadsafe::OutputTracker].
49//!
50//! That summarizes the basic usage of [`OutputSubject`][non_threadsafe::OutputSubject]
51//! and [`OutputTracker`][non_threadsafe::OutputTracker]. This API is provided
52//! in a threadsafe and a non-threadsafe variant. Both variants have the same
53//! API. The difference is in the implementation whether the struct can be sent
54//! and synced over different threads or not. For details on how to use the two
55//! variants see the chapter "Threadsafe and non-threadsafe variants" down
56//! below.
57//!
58//! ## Example
59//!
60//! Let's assume we have production code that uses an adapter called
61//! `MessageSender` to send messages to the outside world.
62//!
63//! ```no_run
64//! struct DomainMessage {
65//!     subject: String,
66//!     content: String,
67//! }
68//!
69//! #[derive(thiserror::Error, Debug, PartialEq, Eq)]
70//! #[error("failed to send message because {message}")]
71//! struct Error {
72//!     message: String,
73//! }
74//!
75//! struct MessageSender {
76//! }
77//!
78//! impl MessageSender {
79//!     fn send_message(&self, message: DomainMessage) -> Result<(), Error> {
80//!         unimplemented!("here we are sending the message to the outside world")
81//!     }
82//! }
83//! ```
84//!
85//! To be able to test this code without using any infrastructure we make the
86//! code "nullable". This is done by implementing the lowest possible level
87//! that touches the infrastructure for real world usage and in a nulled
88//! variant.
89//!
90//! ```no_run
91//! # struct DomainMessage {
92//! #     subject: String,
93//! #     content: String,
94//! # }
95//! #
96//! # #[derive(thiserror::Error, Debug, PartialEq, Eq)]
97//! # #[error("failed to send message because {message}")]
98//! # struct Error {
99//! #     message: String,
100//! # }
101//! #
102//! # #[derive(thiserror::Error, Debug)]
103//! # #[error("some error occurred in the mail api")]
104//! # struct ApiError;
105//! #
106//! //
107//! // Production code
108//! //
109//!
110//! #[derive(Debug, Clone, PartialEq, Eq)]
111//! struct ApiMessage {
112//!     subject: String,
113//!     content: String,
114//! }
115//!
116//! struct MessageSender {
117//!     mail_api: Box<dyn MailApi>,
118//! }
119//!
120//! impl MessageSender {
121//!     // this constructor function is used in production code
122//!     fn new() -> Self {
123//!         Self {
124//!             mail_api: Box::new(RealMail)
125//!         }
126//!     }
127//!
128//!     // this constructor function is used in tests using the nullable pattern
129//!     fn nulled() -> Self {
130//!         Self {
131//!             mail_api: Box::new(NulledMail)
132//!         }
133//!     }
134//! }
135//!
136//! impl MessageSender {
137//!     fn send_message(&self, message: DomainMessage) -> Result<(), Error> {
138//!         let mail = ApiMessage {
139//!             subject: message.subject,
140//!             content: message.content,
141//!         };
142//!
143//!         // code before and after this call to the `MailApi` is tested by our tests
144//!         let result = self.mail_api.send_mail(mail);
145//!
146//!         result.map_err(|err| Error { message: err.to_string() })
147//!     }
148//! }
149//!
150//! //
151//! // Nullability
152//! //
153//!
154//! trait MailApi {
155//!     fn send_mail(&self, message: ApiMessage) -> Result<(), ApiError>;
156//! }
157//!
158//! struct RealMail;
159//!
160//! impl MailApi for RealMail {fn send_mail(&self, message: ApiMessage) -> Result<(), ApiError> {
161//!         unimplemented!("implementation is left out for the example as it is not executed in tests using nullables")
162//!     }
163//! }
164//!
165//! struct NulledMail;
166//!
167//! impl MailApi for NulledMail {fn send_mail(&self, message: ApiMessage) -> Result<(), ApiError> {
168//!         // nothing to do here in the simplest case
169//!         Ok(())
170//!     }
171//! }
172//! ```
173//!
174//! Now we need some way to assert that the code is actually doing the right
175//! things. This is where the output-tracker is used. To do so we equip the
176//! `MessageSender` with an `OutputSubject`.
177//!
178//! ```no_run
179//! # struct DomainMessage {
180//! #     subject: String,
181//! #     content: String,
182//! # }
183//! #
184//! # #[derive(thiserror::Error, Debug, PartialEq, Eq)]
185//! # #[error("failed to send message because {message}")]
186//! # struct Error {
187//! #     message: String,
188//! # }
189//! #
190//! # #[derive(Debug, Clone, PartialEq, Eq)]
191//! # struct ApiMessage {
192//! #     subject: String,
193//! #     content: String,
194//! # }
195//! #
196//! # #[derive(thiserror::Error, Debug)]
197//! # #[error("some error occurred in the mail api")]
198//! # struct ApiError;
199//! #
200//! # trait MailApi {
201//! #     fn send_mail(&self, message: ApiMessage) -> Result<(), ApiError>;
202//! # }
203//! #
204//! # struct RealMail;
205//! #
206//! # impl MailApi for RealMail {fn send_mail(&self, message: ApiMessage) -> Result<(), ApiError> {
207//! #         unimplemented!("implementation is left out for the example as it is not executed in tests using nullables")
208//! #     }
209//! # }
210//! #
211//! # struct NulledMail;
212//! #
213//! # impl MailApi for NulledMail {fn send_mail(&self, message: ApiMessage) -> Result<(), ApiError> {
214//! #         // nothing to do here in the simplest case
215//! #         Ok(())
216//! #     }
217//! # }
218//! #
219//! use output_tracker::non_threadsafe::{Error as OtError, OutputTracker, OutputSubject};
220//!
221//! struct MessageSender {
222//!     mail_api: Box<dyn MailApi>,
223//!     // the output-subject to create output-trackers from
224//!     message_subject: OutputSubject<ApiMessage>,
225//! }
226//!
227//! impl MessageSender {
228//!     // this constructor function is used in production code
229//!     fn new() -> Self {
230//!         Self {
231//!             mail_api: Box::new(RealMail),
232//!             message_subject: OutputSubject::new(),
233//!         }
234//!     }
235//!
236//!     // this constructor function is used in tests using the nullable pattern
237//!     fn nulled() -> Self {
238//!         Self {
239//!             mail_api: Box::new(NulledMail),
240//!             message_subject: OutputSubject::new(),
241//!         }
242//!     }
243//!
244//!     // function to create output-tracker for tracking sent messages
245//!     fn track_messages(&self) -> Result<OutputTracker<ApiMessage>, OtError> {
246//!         self.message_subject.create_tracker()
247//!     }
248//!
249//!     fn send_message(&self, message: DomainMessage) -> Result<(), Error> {
250//!         let mail = ApiMessage {
251//!             subject: message.subject,
252//!             content: message.content,
253//!         };
254//!
255//!         // code before and after this call to the `MailApi` is tested by our tests
256//!         let result = self.mail_api.send_mail(mail.clone());
257//!
258//!         result.map_err(|err| Error { message: err.to_string() })
259//!             // emit sent mail to all active output-trackers
260//!             .inspect(|()| _ = self.message_subject.emit(mail))
261//!     }
262//! }
263//! ```
264//!
265//! Now we can write a test to verify if a domain message is sent via the
266//! Mail-API.
267//!
268//! ```
269//! # struct DomainMessage {
270//! #     subject: String,
271//! #     content: String,
272//! # }
273//! #
274//! # #[derive(thiserror::Error, Debug, PartialEq, Eq)]
275//! # #[error("failed to send message because {message}")]
276//! # struct Error {
277//! #     message: String,
278//! # }
279//! #
280//! # #[derive(Debug, Clone, PartialEq, Eq)]
281//! # struct ApiMessage {
282//! #     subject: String,
283//! #     content: String,
284//! # }
285//! #
286//! # #[derive(thiserror::Error, Debug)]
287//! # #[error("some error occurred in the mail api")]
288//! # struct ApiError;
289//! #
290//! # use output_tracker::non_threadsafe::{Error as OtError, OutputTracker, OutputSubject};
291//! #
292//! # struct MessageSender {
293//! #     mail_api: Box<dyn MailApi>,
294//! #     // the output-subject to create output-trackers from
295//! #     message_subject: OutputSubject<ApiMessage>,
296//! # }
297//! #
298//! # impl MessageSender {
299//! #     // this constructor function is used in production code
300//! #     fn new() -> Self {
301//! #         Self {
302//! #             mail_api: Box::new(RealMail),
303//! #             message_subject: OutputSubject::new(),
304//! #         }
305//! #     }
306//! #
307//! #     // this constructor function is used in tests using the nullable pattern
308//! #     fn nulled() -> Self {
309//! #         Self {
310//! #             mail_api: Box::new(NulledMail),
311//! #             message_subject: OutputSubject::new(),
312//! #         }
313//! #     }
314//! #
315//! #     // function to create output-tracker for tracking sent messages
316//! #     fn track_messages(&self) -> Result<OutputTracker<ApiMessage>, OtError> {
317//! #         self.message_subject.create_tracker()
318//! #     }
319//! #
320//! #     fn send_message(&self, message: DomainMessage) -> Result<(), Error> {
321//! #         let mail = ApiMessage {
322//! #             subject: message.subject,
323//! #             content: message.content,
324//! #         };
325//! #
326//! #         // code before and after this call to the `MailApi` is tested by our tests
327//! #         let result = self.mail_api.send_mail(mail.clone());
328//! #
329//! #         result.map_err(|err| Error { message: err.to_string() })
330//! #             // emit sent mail to all active output-trackers
331//! #             .inspect(|()| _ = self.message_subject.emit(mail))
332//! #     }
333//! # }
334//! #
335//! # trait MailApi {
336//! #     fn send_mail(&self, message: ApiMessage) -> Result<(), ApiError>;
337//! # }
338//! #
339//! # struct RealMail;
340//! #
341//! # impl MailApi for RealMail {fn send_mail(&self, message: ApiMessage) -> Result<(), ApiError> {
342//! #         unimplemented!("implementation is left out for this example
343//! #                + as it is not executed in tests using nullables")
344//! #     }
345//! # }
346//! #
347//! # struct NulledMail;
348//! #
349//! # impl MailApi for NulledMail {
350//! #     fn send_mail(&self, message: ApiMessage) -> Result<(), ApiError> {
351//! #         // nothing to do here in the simplest case
352//! #         Ok(())
353//! #     }
354//! # }
355//! #
356//! # fn main() {
357//! #     domain_message_is_sent_via_the_mail_api();
358//! # }
359//! #
360//! //#[test]
361//! fn domain_message_is_sent_via_the_mail_api() {
362//!     //
363//!     // Arrange
364//!     //
365//!
366//!     // set up nulled `MessageSender`
367//!     let message_sender = MessageSender::nulled();
368//!
369//!     // create an output-tracker to track sent messages
370//!     let message_tracker = message_sender.track_messages()
371//!         .unwrap_or_else(|err| panic!("could not create message tracker because {err}"));
372//!
373//!     //
374//!     // Act
375//!     //
376//!
377//!     let message = DomainMessage {
378//!         subject: "Monthly report for project X".into(),
379//!         content: "Please provide the monthly report for project X due by end of the week".into(),
380//!     };
381//!
382//!     let result = message_sender.send_message(message);
383//!
384//!     //
385//!     // Assert
386//!     //
387//!
388//!     assert_eq!(result, Ok(()));
389//!
390//!     // read the output from the message tracker
391//!     let output = message_tracker.output()
392//!         .unwrap_or_else(|err| panic!("could not read output of message tracker because {err}"));
393//!
394//!     assert_eq!(output, vec![
395//!         ApiMessage {
396//!             subject: "Monthly report for project X".into(),
397//!             content: "Please provide the monthly report for project X due by end of the week".into(),
398//!         }
399//!     ])
400//! }
401//! ```
402//!
403//! See the integration tests of this crate as they demonstrate the usage of
404//! output-tracker in a more involved and complete way.
405//!
406//! ## Threadsafe and non-threadsafe variants
407//!
408//! The output-tracker functionality is provided in a non-threadsafe variant and
409//! a threadsafe one. The different variants are gated behind crate features and
410//! can be activated as needed. The API of the two variants is interchangeable.
411//! That is the struct names and functions are identical for both variants. The
412//! module from which the structs are imported determines which variant is going
413//! to be used.
414//!
415//! By default, only the non-threadsafe variant is compiled. One can activate
416//! only one variant or both variants as needed. If the feature `threadsafe` is
417//! specified, only the threadsafe variant is compiled. To use both variants at
418//! the same time both features must be specified. The crate features and the
419//! variants which are activated by each feature are listed in the table below.
420//!
421//! | Crate feature    | Variant        | Rust module import                                        |
422//! |:-----------------|:---------------|:----------------------------------------------------------|
423//! | `non-threadsafe` | non-threadsafe | [`use output_tracker::non_threadsafe::*`][non_threadsafe] |
424//! | `threadsafe`     | threadsafe     | [`use output_tracker::threadsafe::*`][threadsafe]         |
425//!
426//! [nullables]: https://www.jamesshore.com/v2/projects/nullables
427
428#![doc(html_root_url = "https://docs.rs/output-tracker/0.1.1")]
429
430mod inner_subject;
431mod inner_tracker;
432#[cfg(any(feature = "non-threadsafe", not(feature = "threadsafe")))]
433pub mod non_threadsafe;
434#[cfg(feature = "threadsafe")]
435pub mod threadsafe;
436mod tracker_handle;
437
438// test code snippets in the README.md
439#[cfg(doctest)]
440#[doc = include_str!("../README.md")]
441#[allow(dead_code)]
442type TestExamplesInReadme = ();
443
444// workaround for false positive 'unused extern crate' warnings until
445// Rust issue [#95513](https://github.com/rust-lang/rust/issues/95513) is fixed
446#[cfg(test)]
447mod dummy_extern_uses {
448    use version_sync as _;
449}