# Testing Deciders

In this step, you **test the deciders** from **[Making Decisions](/learn/tutorials/first-steps-with-architecturekit/rust/making-decisions)** – without a database.

## Given, When, Then

A decider receives a command and a state, and the state is built from events. So every test of a decider can be told in three parts: **given** the events that have happened so far, **when** a command arrives, **then** the decider writes these events, or rejects the command. The module **`architecturekit::testing`** spells out tests exactly this way, and it builds the state from the given events itself, without EventSourcingDB.

The module needs the feature `testing`. Only the tests need it, not the program, so turn it on for the tests alone, as a dev dependency:

```shell
cargo add architecturekit --dev --features testing
```

Create a file `tests/deciders.rs`:

```rust
use architecturekit::testing::given;
use architecturekit::{Category, Precondition};
use library::book::BookEvent;
use library::commands::{BorrowBook, ReturnBook};
use library::deciders::{borrow_book, return_book};
use library::events::{BookAcquired, BookBorrowed, BookReturned};

fn book_acquired() -> BookEvent {
  BookEvent::Acquired(BookAcquired {
    title: "2001 – A Space Odyssey".to_owned(),
    author: "Arthur C. Clarke".to_owned(),
    isbn: "978-0756906788".to_owned(),
  })
}

mod borrow_book {
  use super::*;

  #[tokio::test]
  async fn lends_an_available_book() {
    given(&borrow_book(), [book_acquired()])
      .when(BorrowBook {
        book_id: "42".to_owned(),
        reader_id: "23".to_owned(),
        borrowed_until: "2026-10-24".to_owned(),
      })
      .await
      .then_events([BookEvent::Borrowed(BookBorrowed {
        borrowed_by: "23".to_owned(),
        borrowed_until: "2026-10-24".to_owned(),
      })])
      .then_preconditions([Precondition::on_state_read()]);
  }

  #[tokio::test]
  async fn rejects_a_book_that_is_already_borrowed() {
    given(
      &borrow_book(),
      [
        book_acquired(),
        BookEvent::Borrowed(BookBorrowed {
          borrowed_by: "23".to_owned(),
          borrowed_until: "2026-10-24".to_owned(),
        }),
      ],
    )
    .when(BorrowBook {
      book_id: "42".to_owned(),
      reader_id: "17".to_owned(),
      borrowed_until: "2026-10-31".to_owned(),
    })
    .await
    .then_failed_in(Category::Domain);
  }

  #[tokio::test]
  async fn rejects_a_book_that_does_not_exist() {
    given(&borrow_book(), [])
      .when(BorrowBook {
        book_id: "42".to_owned(),
        reader_id: "23".to_owned(),
        borrowed_until: "2026-10-24".to_owned(),
      })
      .await
      .then_failed_in(Category::Domain);
  }
}

mod return_book {
  use super::*;

  #[tokio::test]
  async fn returns_a_borrowed_book() {
    given(
      &return_book(),
      [
        book_acquired(),
        BookEvent::Borrowed(BookBorrowed {
          borrowed_by: "23".to_owned(),
          borrowed_until: "2026-10-24".to_owned(),
        }),
      ],
    )
    .when(ReturnBook { book_id: "42".to_owned() })
    .await
    .then_events([BookEvent::Returned(BookReturned {})]);
  }

  #[tokio::test]
  async fn does_nothing_for_a_book_that_is_not_borrowed() {
    given(&return_book(), [book_acquired()])
      .when(ReturnBook { book_id: "42".to_owned() })
      .await
      .then_nothing();
  }
}
```

Each test is a function marked with **`#[tokio::test]`**, which runs it on Tokio, since a decider decides asynchronously. A module groups the tests of each decider, and `use super::*` brings in what the file imports, together with `book_acquired`, which builds the event that most tests start from.

**`given`** takes the decider and the events that have happened so far, and **`when`** takes the command. Once awaited, `when` returns the **decision** of the decider on the command: events, nothing, or a rejection. The functions that start with `then_` check it, and since each of them returns the decision again, you can chain them:

- **`then_events`** expects exactly the given events, in the given order. It compares them by their type and their data as JSON, which is what would be stored, so the events need no `PartialEq`.
- **`then_failed_in`** expects an error of the given category. Here, that is **`Category::Domain`**, to which every `DomainError` belongs, whatever its message. To tell one rejection apart from the others, see **[Expecting Rejections](/docs/architecturekit/rust/testing-deciders#expecting-rejections)**.
- **`then_nothing`** expects neither events nor an error.
- **`then_preconditions`** expects exactly the given preconditions, here the `on_state_read` precondition from **[Making Decisions](/learn/tutorials/first-steps-with-architecturekit/rust/making-decisions)**. The test names it with `Precondition::on_state_read`, just as the command does, since two preconditions are equal if they check the same thing.

## Running the Tests

Run the tests. They need neither EventSourcingDB nor the API, so you can stop both first:

```shell
cargo test
```

Cargo runs the tests of every part of the package. The library crate and the program have no tests of their own, and neither has their documentation, so all five tests come from `tests/deciders.rs`, and pass. The tests run in parallel, so their order varies, as do the hashes in the file names and the durations:

```text
     Running unittests src/lib.rs (target/debug/deps/library-5a1c23d119c571f2)

running 0 tests

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

     Running unittests src/main.rs (target/debug/deps/library-73631a88eff44574)

running 0 tests

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

     Running tests/deciders.rs (target/debug/deps/deciders-7ce093866d940d9f)

running 5 tests
test return_book::returns_a_borrowed_book ... ok
test borrow_book::rejects_a_book_that_does_not_exist ... ok
test borrow_book::lends_an_available_book ... ok
test borrow_book::rejects_a_book_that_is_already_borrowed ... ok
test return_book::does_nothing_for_a_book_that_is_not_borrowed ... ok

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

   Doc-tests library

running 0 tests

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

## When a Test Fails

To see that the tests actually guard the rules, remove the check for a borrowed book from `borrow_book` in `src/deciders.rs`:

```rust
if book.is_borrowed {
  return Err(DomainError::new(format!("book {} is already borrowed", cmd.book_id)));
}
```

Run the tests again:

```shell
cargo test
```

The decider now lends a book that is already borrowed, and the test tells you so, and where. Cargo stops after the tests that failed, so the documentation is not tested. Again, the order, the hashes, the number of the thread, and the durations vary:

```text
     Running unittests src/lib.rs (target/debug/deps/library-5a1c23d119c571f2)

running 0 tests

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

     Running unittests src/main.rs (target/debug/deps/library-73631a88eff44574)

running 0 tests

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

     Running tests/deciders.rs (target/debug/deps/deciders-7ce093866d940d9f)

running 5 tests
test return_book::returns_a_borrowed_book ... ok
test borrow_book::lends_an_available_book ... ok
test return_book::does_nothing_for_a_book_that_is_not_borrowed ... ok
test borrow_book::rejects_a_book_that_does_not_exist ... ok
test borrow_book::rejects_a_book_that_is_already_borrowed ... FAILED

failures:

---- borrow_book::rejects_a_book_that_is_already_borrowed stdout ----

thread 'borrow_book::rejects_a_book_that_is_already_borrowed' (75683273) panicked at tests/deciders.rs:53:6:
expected an error of the category Domain, got 1 event(s): ["io.eventsourcingdb.library.book-borrowed"]
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    borrow_book::rejects_a_book_that_is_already_borrowed

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

error: test failed, to rerun pass `--test deciders`
```

Put the check back, and the tests pass again. For more ways to check a decision, see **[Testing Deciders](/docs/architecturekit/rust/testing-deciders)**, and for why tests like these matter, see **[Testing Event-Sourced Systems](/concepts/testing-event-sourced-systems)**.

## Shutting Down

If they are still running, press Ctrl+C in the terminals of the API and of EventSourcingDB. Since the data directory of EventSourcingDB is temporary, all events are gone afterwards.

## Next Steps

You have built a small library with ArchitectureKit, from events to a tested HTTP API. To go further:

- **[Testing Projections](/docs/architecturekit/rust/testing-projections)** and **[Testing Queries](/docs/architecturekit/rust/testing-queries)** test the reading side without a database as well.
- **[Using Preconditions](/docs/architecturekit/rust/using-preconditions)** shows how to check the revision a caller has seen, and how to enforce rules across books, such as acquiring every ISBN only once.
- **[Setting Up an HTTP API](/docs/architecturekit/rust/setting-up-an-http-api)** and **[Handling Commands over HTTP](/docs/architecturekit/rust/handling-commands-over-http)** add users, and refuse commands they are not allowed to send.
- **[Versioning Events](/docs/architecturekit/rust/versioning-events)** changes the shape of an event without changing the events that are already stored.
