# Testing a whole journey

## What you want to do

You want to run one order through the shop's real steps and policies, from start to end, and check the way its journey went: which steps succeeded, which failed and why, and how the journey ended. The bank and the carriers should be fakes, so the test never reaches the outside world.

## The code

The program is [chapter 25](https://itinera-dev.org/book/rust/25-reading-the-result.md)'s shop, with its tests in a module that only a test build compiles, as in [chapter 27](https://itinera-dev.org/book/rust/27-testing-a-step-alone.md). The tests bring their own bank, which charges or is down, and their own carrier, which collects every parcel or none, implementing `Payments` and `Carrier`, with their errors, as [chapter 14](https://itinera-dev.org/book/rust/14-repairing-then-trying-once-more.md) and [chapter 12](https://itinera-dev.org/book/rust/12-trying-again.md) declared them:

Rust, from the program [`ch30_testing_a_whole_journey`](https://github.com/itinera-dev/book/blob/main/rust/tested/ch30_testing_a_whole_journey.rs)

```rust
/// A bank for tests: it charges every card, or its network is down.
#[derive(Clone, Copy)]
enum TestBank {
    Charges,
    IsDown,
}

impl Payments for TestBank {
    fn charge(&self, order: &str, _: &str, _: i64) -> Result<Option<String>, NetworkDown> {
        match self {
            TestBank::Charges => Ok(Some(format!("R-{order}"))),
            TestBank::IsDown => Err(NetworkDown),
        }
    }

    fn replacement(&self, _: &str) -> Option<String> {
        None
    }
}

/// A carrier for tests: it books every parcel, and collects every parcel or none.
#[derive(Clone, Copy)]
enum TestCarrier {
    CollectsAll,
    CollectsNone,
}

impl Carrier for TestCarrier {
    fn book(&self, order: &str, _: i64) -> Result<String, BookingFailed> {
        Ok(format!("T-{order}"))
    }

    fn collects(&self, _: i64) -> bool {
        matches!(self, TestCarrier::CollectsAll)
    }
}
```

Rust, the same in its asynchronous form, from the program [`ch30_testing_a_whole_journey_async`](https://github.com/itinera-dev/book/blob/main/rust/tested/ch30_testing_a_whole_journey_async.rs)

```rust
/// A bank for tests: it charges every card, or its network is down.
#[derive(Clone, Copy)]
enum TestBank {
    Charges,
    IsDown,
}

#[async_trait]
impl Payments for TestBank {
    async fn charge(
        &self,
        order: &str,
        _: &str,
        _: i64,
    ) -> Result<Option<String>, NetworkDown> {
        match self {
            TestBank::Charges => Ok(Some(format!("R-{order}"))),
            TestBank::IsDown => Err(NetworkDown),
        }
    }

    async fn replacement(&self, _: &str) -> Option<String> {
        None
    }
}

/// A carrier for tests: it books every parcel, and collects every parcel or none.
#[derive(Clone, Copy)]
enum TestCarrier {
    CollectsAll,
    CollectsNone,
}

#[async_trait]
impl Carrier for TestCarrier {
    async fn book(&self, order: &str, _: i64) -> Result<String, BookingFailed> {
        Ok(format!("T-{order}"))
    }

    async fn collects(&self, _: i64) -> bool {
        matches!(self, TestCarrier::CollectsAll)
    }
}
```

Each test runs an order through `path_of`, which makes an instance of the shop's workflow, as `main` does, with two more calls on its builder: `fake_provider`, which comes with the `testing` feature, and `reporter`, which any journey's builder has. `fake_provider` names one of the workflow's providers, `bank`, from [chapter 7](https://itinera-dev.org/book/rust/07-giving-a-step-the-tools-it-needs.md) as [chapter 12](https://itinera-dev.org/book/rust/12-trying-again.md) shows it, or `carriers`, from [chapter 13](https://itinera-dev.org/book/rust/13-a-carrier-for-each-attempt.md), and gives a function the executor calls in its place; the type in `::<...>` is the service it provides. `reporter` adds a `RecordingReporter`, from `itinera::report`, which also comes with the `testing` feature, to the journey's reporters. It keeps every event, and its clones share what they keep, so the test reads the events from the clone it kept. Its `path` gives the journey's path:

Rust, from the program [`ch30_testing_a_whole_journey`](https://github.com/itinera-dev/book/blob/main/rust/tested/ch30_testing_a_whole_journey.rs)

```rust
/// Runs an order of 30 with a parcel of this weight, through the shop's real steps and
/// policies, with the test's bank and carriers in place of the shop's, and gives back its
/// journey's path.
fn path_of(
    order: &str,
    weight: i64,
    bank: TestBank,
    carrier: TestCarrier,
) -> Result<Vec<String>, Error> {
    let recorder = RecordingReporter::default();
    let instance = Orders::builder()
        .channel("email")
        .initial_data(NewOrder {
            order: order.to_owned(),
            customer: "Ann".to_owned(),
            price: 30,
            discount: None,
            parcel: Parcel {
                weight,
                size: [20, 15, 5],
            },
            card: Some("ann-2028".to_owned()),
            gift_card: false,
        })
        .reporter(recorder.clone())
        .fake_provider::<dyn Payments>("bank", move || Ok(provide!(bank)))
        .fake_provider::<dyn Carrier>("carriers", move || Ok(provide!(carrier)))
        .create()?;
    local_executor_for!(Orders).run(instance)?;
    Ok(recorder.path())
}
```

Rust, the same in its asynchronous form, from the program [`ch30_testing_a_whole_journey_async`](https://github.com/itinera-dev/book/blob/main/rust/tested/ch30_testing_a_whole_journey_async.rs)

```rust
/// Runs an order of 30 with a parcel of this weight, through the shop's real steps and
/// policies, with the test's bank and carriers in place of the shop's, and gives back its
/// journey's path.
async fn path_of(
    order: &str,
    weight: i64,
    bank: TestBank,
    carrier: TestCarrier,
) -> Result<Vec<String>, Error> {
    let recorder = RecordingReporter::default();
    let instance = Orders::builder()
        .channel("email")
        .initial_data(NewOrder {
            order: order.to_owned(),
            customer: "Ann".to_owned(),
            price: 30,
            discount: None,
            parcel: Parcel {
                weight,
                size: [20, 15, 5],
            },
            card: Some("ann-2028".to_owned()),
            gift_card: false,
        })
        .reporter(recorder.clone())
        .fake_provider::<dyn Payments>("bank", move || Ok(provide!(bank)))
        .fake_provider::<dyn Carrier>("carriers", move || Ok(provide!(carrier)))
        .create()?;
    local_executor_for!(Orders).run(instance).await?;
    Ok(recorder.path())
}
```

`NewOrder` is as [chapter 25](https://itinera-dev.org/book/rust/25-reading-the-result.md) shows it, and `Parcel` as [chapter 21](https://itinera-dev.org/book/rust/21-deciding-where-a-steps-data-comes-from.md) does.

The shop approves the path of each kind of order once, and writes it down. The paths follow the workflow as [chapter 23](https://itinera-dev.org/book/rust/23-undoing-a-charge-when-the-shipping-fails.md) declared it, with `outage` from [chapter 17](https://itinera-dev.org/book/rust/17-giving-up-a-step-early.md). Each test compares a journey's path with the approved one:

Rust, from the program [`ch30_testing_a_whole_journey`](https://github.com/itinera-dev/book/blob/main/rust/tested/ch30_testing_a_whole_journey.rs)

```rust
#[test]
fn an_order_the_bank_charges_and_the_carrier_takes_is_shipped() -> Result<(), Error> {
    assert_eq!(
        path_of("A-1", 2, TestBank::Charges, TestCarrier::CollectsAll)?,
        [
            "lookup attempt 1 succeeded",
            "price attempt 1 succeeded",
            "charge attempt 1 succeeded",
            "ship attempt 1 succeeded",
            "journey succeeded",
        ]
    );
    Ok(())
}

#[test]
fn a_parcel_over_the_limit_is_given_up_after_its_first_attempt() -> Result<(), Error> {
    assert_eq!(
        path_of("A-2", 90, TestBank::Charges, TestCarrier::CollectsAll)?,
        [
            "lookup attempt 1 succeeded",
            "price attempt 1 succeeded",
            "charge attempt 1 succeeded",
            "ship attempt 1 failed: over-parcel-limit",
            "ship given up: FailWorkflow from failed_shipping",
            "journey failed at ship: no-carrier-can-take-it",
        ]
    );
    Ok(())
}

#[test]
fn a_charge_whose_bank_is_down_is_given_up_and_never_rescued() -> Result<(), Error> {
    assert_eq!(
        path_of("A-3", 2, TestBank::IsDown, TestCarrier::CollectsAll)?,
        [
            "lookup attempt 1 succeeded",
            "price attempt 1 succeeded",
            "charge attempt 1 terminated abnormally: the payment network is down",
            "charge given up: FailWorkflow from outage",
            "journey failed at charge: payment-network-down",
        ]
    );
    Ok(())
}

#[test]
fn a_parcel_no_carrier_collects_is_tried_once_more_then_given_up() -> Result<(), Error> {
    assert_eq!(
        path_of("A-4", 60, TestBank::Charges, TestCarrier::CollectsNone)?,
        [
            "lookup attempt 1 succeeded",
            "price attempt 1 succeeded",
            "charge attempt 1 succeeded",
            "ship attempt 1 failed: too-heavy",
            "ship retrying",
            "ship attempt 2 failed: too-heavy",
            "ship given up: retries exhausted",
            "journey failed at ship: too-heavy",
        ]
    );
    Ok(())
}
```

Rust, the same in its asynchronous form, from the program [`ch30_testing_a_whole_journey_async`](https://github.com/itinera-dev/book/blob/main/rust/tested/ch30_testing_a_whole_journey_async.rs)

```rust
#[tokio::test]
async fn an_order_the_bank_charges_and_the_carrier_takes_is_shipped() -> Result<(), Error> {
    assert_eq!(
        path_of("A-1", 2, TestBank::Charges, TestCarrier::CollectsAll).await?,
        [
            "lookup attempt 1 succeeded",
            "price attempt 1 succeeded",
            "charge attempt 1 succeeded",
            "ship attempt 1 succeeded",
            "journey succeeded",
        ]
    );
    Ok(())
}

#[tokio::test]
async fn a_parcel_over_the_limit_is_given_up_after_its_first_attempt() -> Result<(), Error> {
    assert_eq!(
        path_of("A-2", 90, TestBank::Charges, TestCarrier::CollectsAll).await?,
        [
            "lookup attempt 1 succeeded",
            "price attempt 1 succeeded",
            "charge attempt 1 succeeded",
            "ship attempt 1 failed: over-parcel-limit",
            "ship given up: FailWorkflow from failed_shipping",
            "journey failed at ship: no-carrier-can-take-it",
        ]
    );
    Ok(())
}

#[tokio::test]
async fn a_charge_whose_bank_is_down_is_given_up_and_never_rescued() -> Result<(), Error> {
    assert_eq!(
        path_of("A-3", 2, TestBank::IsDown, TestCarrier::CollectsAll).await?,
        [
            "lookup attempt 1 succeeded",
            "price attempt 1 succeeded",
            "charge attempt 1 terminated abnormally: the payment network is down",
            "charge given up: FailWorkflow from outage",
            "journey failed at charge: payment-network-down",
        ]
    );
    Ok(())
}

#[tokio::test]
async fn a_parcel_no_carrier_collects_is_tried_once_more_then_given_up() -> Result<(), Error> {
    assert_eq!(
        path_of("A-4", 60, TestBank::Charges, TestCarrier::CollectsNone).await?,
        [
            "lookup attempt 1 succeeded",
            "price attempt 1 succeeded",
            "charge attempt 1 succeeded",
            "ship attempt 1 failed: too-heavy",
            "ship retrying",
            "ship attempt 2 failed: too-heavy",
            "ship given up: retries exhausted",
            "journey failed at ship: too-heavy",
        ]
    );
    Ok(())
}
```

In the asynchronous form, which tests [chapter 26](https://itinera-dev.org/book/rust/26-running-async-journeys-while-others-wait.md)'s shop, the fakes implement the services with `#[async_trait]` and `async fn`, `path_of` runs the instance on the executor `local_executor_for!` makes, an `AsyncLocalExecutor`, and awaits it, and each test is an `async fn` marked `#[tokio::test]`.

As in [chapter 29](https://itinera-dev.org/book/rust/29-testing-the-flow-with-scripted-steps.md), `cargo test` hides what the shop's log, its desk and its messages print. It prints the same for both forms:

What running the tests of `ch30_testing_a_whole_journey` prints, recorded in [`ch30_testing_a_whole_journey.txt`](https://github.com/itinera-dev/book/blob/main/rust/tested/ch30_testing_a_whole_journey.txt)

```text
running 4 tests
test tests::a_charge_whose_bank_is_down_is_given_up_and_never_rescued ... ok
test tests::a_parcel_no_carrier_collects_is_tried_once_more_then_given_up ... ok
test tests::a_parcel_over_the_limit_is_given_up_after_its_first_attempt ... ok
test tests::an_order_the_bank_charges_and_the_carrier_takes_is_shipped ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
```

## How it works

1. **A fake provider replaces only the provider.** The workflow keeps its providers' keys and scopes. The steps still ask for `payments` and `carrier`, and `carriers` stays a provider made for every attempt. Only what the provider returns changes.
2. **Everything else is real.** The steps run their own rules: the shipping refuses the parcel of 90 kg itself, with `over-parcel-limit`, as [chapter 27](https://itinera-dev.org/book/rust/27-testing-a-step-alone.md)'s test showed. The policies decide, the adapter supplies the weight, and the result and the events are the ones a real order would give.
3. **A path is the journey's flow, one line for each event of it.** `path` reads only the events that say how the flow went: each attempt's outcome, each decision about a step, and how the journey ended. Everything else, such as which provider served a step or which data was committed, is left out, so a path changes only when the flow does.
4. **Each line is a plain sentence.** `charge attempt 1 terminated abnormally` names the step, its attempt and how it ended, with the reason or the error after a colon. `ship given up: FailWorkflow from failed_shipping` names the cause, and the policy that decided it. `journey failed at ship: no-carrier-can-take-it` names the step where the journey ended, and the reason it ended with: after retries are exhausted, the last attempt's reason, `too-heavy` for the parcel no carrier collects.
5. **An approved path is a decision of the flow, written down.** A person reads it once and agrees that this is what should happen to such an order. If a change to the workflow sends an order another way, the test fails. A change to a step's rules that keeps the flow keeps the path.

So the shop tests each piece alone, its flow with scripts, and whole journeys by their paths. But the shop's log will soon write to a logging service, over the network too, where a write can fail.

## Outcomes

We have shown how to test a whole journey, with fake providers in place of the workflow's, by comparing its path with an approved one.

| The usual way                                                                                                   | With Itinera                                                                                                       |
| --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Testing a whole request often needs a test double for every dependency, wired by hand into the code under test. | `fake_provider` replaces a workflow's provider by name, and the steps get the fake as they would the real service. |
| An end-to-end test usually checks the final state, and says little about the way a request went.                | A journey's path lists each outcome and decision in order, and a test compares it with the path the shop approved. |
