Testing with a Database
Some tests need a real database, for example to run commands from end to end.
To get one, turn on the feature testcontainers for the tests, which starts EventSourcingDB in a container, and therefore needs Docker. It turns on the feature testing as well:
cargo add architecturekit --dev --features testcontainers
Call the testing::store function with the source and the schemas to register. It returns a store on a database that all tests of the program share, which the first test that asks for it starts in a container, while the others wait for that. The example gives the book a random ID of the uuid crate, which you add with cargo add uuid --dev --features v4:
use architecturekit::{Events, testing};
use uuid::Uuid;
#[tokio::test]
async fn acquires_a_book() {
let store = testing::store("https://library.eventsourcingdb.io", BookEvent::schemas()).await;
store
.execute(
&acquire_book(),
AcquireBook {
book: BOOKS.build(&[Uuid::new_v4().to_string()]),
title: "2001 – A Space Odyssey".to_owned(),
author: "Arthur C. Clarke".to_owned(),
isbn: "978-0756906788".to_owned(),
},
)
.await
.expect("the book is acquired");
}
To build the store with options, such as state_cache, call the testing::store_with function, with a function that sets them on the builder, so that a test runs with the same store as the application:
let store =
testing::store_with("https://library.eventsourcingdb.io", BookEvent::schemas(), |builder| {
builder.state_cache(1_000)
})
.await;
The shared database keeps running beyond the test that started it, until the program ends, so there is nothing to stop once the tests have run. Then the reaper of Testcontainers, Ryuk, which runs in a container of its own, removes it a few seconds later, however the program ends, even if it is killed. The database is registered with Ryuk before it starts, so that holds even for a program that ends while the database is still starting. Ryuk is configured with environment variables: TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED set to true starts it privileged, TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE names the socket of Docker where it does not live at /var/run/docker.sock, and TESTCONTAINERS_RYUK_DISABLED set to true turns it off, which leaves the database behind.
Note that the schemas come as one argument. To register the schemas of several enums, chain them, for example BookEvent::schemas().into_iter().chain(ReaderEvent::schemas()).
The tests share the events as well, so a test writes to subjects of its own, for example with a random ID in them, and reads only from those. A test that reads more than that, such as a projection from /, needs a database of its own. Call Database::isolated to start one for the test alone, and ask it for a store. Its container stops once the database is dropped, so keep it in a local variable for as long as the test runs. It takes about a second to start, so use it only where the shared one would not do:
use architecturekit::testing::Database;
let database = Database::isolated().await;
let store = database.store("https://library.eventsourcingdb.io", BookEvent::schemas()).await;
For a test that connects by itself, such as one that starts a whole server, call Database::shared for the shared database, or Database::isolated. The url and api_token methods of a Database return what a client needs. Its client method returns a client, for example to write an event that no command would, and its store and store_with methods return a store, as above. Here, Config stands for the configuration of your application:
let database = Database::shared().await;
let config =
Config { database_url: database.url().to_string(), api_token: database.api_token().to_owned() };
Note that every test that asks for a database fails if the database can not be started, for example since Docker is not running, with the message of the failure. A failed start of the shared database is not tried again: every test that asks for it later fails with the same message. To run the other tests without Docker, mark the tests that need a database with #[ignore], and run them where Docker is available, with cargo test -- --include-ignored.
Note that the database runs the image thenativeweb/eventsourcingdb with the tag that the environment variable EVENTSOURCINGDB_IMAGE_TAG names, or latest without it, as for the tests of the client SDK, for example EVENTSOURCINGDB_IMAGE_TAG=preview cargo test. Docker pulls the image only if it is missing, so which release a tag stands for depends on what the machine has pulled before. The database runs without a signing key, so its events carry no signature, and a store with signature_verification fails to read them with Permanent::Unverified. Signature verification can therefore not be tested with it (see Verifying Events).
Note that testcontainers is a feature of its own because it brings along Testcontainers and a client of Docker. Tests that only use the feature testing, for example to test deciders and projections, build without either.