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}