Docs / ArchitectureKit / Rust / Guides / Testing Projections

Testing Projections

To test a projection without a database, call the project function of the module architecturekit::testing with the projection and the stored events, which needs the feature testing (see Testing Deciders).

To turn typed events for the same subject into stored ones, call the stored_events function, which numbers them from 0. A projection applies events asynchronously, so a test runs on Tokio, and waits for project. The expect_items function then expects the view to hold exactly the given items, in the given order. It compares them with ==, as assert_eq! does, so the items implement PartialEq and Debug, as BookItem does (see Defining Views):

use architecturekit::testing::{expect_items, project, stored_events};

#[tokio::test]
async fn projects_the_catalog() {
  let catalog_projection = CatalogProjection { catalog: new_catalog() };

  project(
    &catalog_projection,
    &stored_events(
      &Subject::new("/books/42"),
      [
        BookEvent::Acquired(BookAcquired {
          title: "2001 – A Space Odyssey".to_owned(),
          author: "Arthur C. Clarke".to_owned(),
          isbn: "978-0756906788".to_owned(),
        }),
        BookEvent::Borrowed(BookBorrowed {
          borrowed_by: "23".to_owned(),
          borrowed_until: "2026-10-24".to_owned(),
        }),
      ],
    ),
  )
  .await
  .expect("the events are applied");

  expect_items(
    &catalog_projection.catalog,
    [BookItem {
      id: "42".to_owned(),
      title: "2001 – A Space Odyssey".to_owned(),
      author: "Arthur C. Clarke".to_owned(),
      is_borrowed: true,
      borrowed_until: "2026-10-24".to_owned(),
      revision: Some(Revision::from(1)),
    }],
  )
  .await;
}

project hands over the events as they are stored, and they take the same way as in a run: the upcasters of the projection run, an event of a type that the projection has no variant for is left out, an event that an upcaster splits arrives part by part, and the projection learns of every event in seen. To test that a projection handles an older event type, hand over an event of that type, made with StoredEvent::raw (see Testing Upcasters). project takes StoredEvents, owned or borrowed, as well as events of the client SDK, such as those read from a database.

If the projection refuses an event, project returns the failure, rather than failing the test, so that a test can check that a projection refuses an event. expect fails the test with it, as above. The failure names the event by its ID, as in applying event 1: …. A panic of the projection is returned the same way, with its message, as in permanent failure: applying event 1: the projection panicked: …, rather than taking the test down with it.

To get the items as a Vec instead, call the items_of function:

use architecturekit::testing::items_of;

let items = items_of(&catalog_projection.catalog).await;

Both fail the test if the view fails while it is read, also after some of the items.

If a projection reads the time of an event, or the events of several subjects have to follow one another, call the stored_events_at function instead of stored_events. It numbers the events from the given ID on, and times them one minute apart from the given time on, which is a DateTime<Utc> of the chrono crate:

use architecturekit::testing::stored_events_at;
use chrono::{TimeDelta, TimeZone, Utc};

let noon = Utc.with_ymd_and_hms(2026, 3, 2, 12, 0, 0).unwrap();

let events = [
  stored_events_at(&Subject::new("/books/42"), 0, noon, [book_acquired()]),
  stored_events_at(&Subject::new("/books/23"), 1, noon + TimeDelta::minutes(1), [book_acquired()]),
]
.concat();

Here, book_acquired returns a BookAcquired event, as under Testing Deciders. The fields of a StoredEvent are public, so that a test can also give an event a type that the projection does not know, data that it can not read, or a time of its own.

How a projection is run depends on the method that starts it, which the compiler checks against the traits the projection implements (see Resuming Projections), so there is nothing to check about it in a test. project drives a resumable projection as well, but saves no checkpoint.

To test a transactional projection, call the project_transactional function instead of project. It begins a transaction, applies the events, and commits the transaction with the ID of the last event. If an event is refused, or the projection panics, it rolls the transaction back, and returns the failure, naming the event as project does. Here, book_table is a TransactionalBookTableProjection (see Resuming Projections):

use architecturekit::testing::project_transactional;

project_transactional(
  &book_table,
  &stored_events(
    &Subject::new("/books/42"),
    [BookEvent::Acquired(BookAcquired {
      title: "2001 – A Space Odyssey".to_owned(),
      author: "Arthur C. Clarke".to_owned(),
      isbn: "978-0756906788".to_owned(),
    })],
  ),
)
.await
.expect("the events are applied");

Note that project_transactional begins no transaction without events, and that a failure of the rollback is reported alongside the failure that caused it.