1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
/*! A `std::future::Future` and `Result`-based testing trait for managing the lifecycle of stateful, asynchronous tests. ## Why `spekt`: Most Rust unit tests terminate based on `panic`s (generally triggered by `assert!`), with resource clean-up implemented manually through the `Drop` trait. Working synchronously with a stateful resource like a database might look like this: ``` use postgres::{Client, NoTls, Row, error::Error as PostgresError}; struct PostgresTest { client: Client } impl PostgresTest { fn new() -> Self { let mut client = Client::connect("host=localhost user=postgres", NoTls).expect("Error connecting to database"); Self { client } } fn add_test_table(&self) -> Result<Row, PostgresError> { self.client.batch_execute("CREATE TABLE my_test_table ()") } } impl Drop for PostgresTest { fn drop(&mut self) { self.client.batch_execute("DROP TABLE my_test_table").expect("Error cleaning up test table"); } } #[test] fn adds_queryable_test_table() { let client = TestClient::new(); let create_response = client.adds_test_table(); assert!(create_response.is_ok(), "Error creating test table"); let query_response = client.query("SELECT FROM my_test_table"); assert!(query_response.is_ok(), "Error creating test table"); } ``` While this works for many cases, there are a couple of issues with this recommendation: 1. Technically, Rust doesn't _guarantee_ that `Drop` will be run, and [one shouldn't rely on `Drop` to be run in all cases](http://cglab.ca/%7Eabeinges/blah/everyone-poops/). 2. `Drop` also cannot be asynchronous! There has been much discussion around [Asynchronous destructors](https://internals.rust-lang.org/t/asynchronous-destructors/11127), but no reliable destructor trait has yet materialized for `async` functions. 3. `panic`-based assertions (and their associated unwinding) also behave in ways that [might be unpredictable across runtimes](https://github.com/tokio-rs/tokio/issues/2002). This is, specifically, an [issue in tests](https://github.com/tokio-rs/tokio/issues/2699) for which there is no good universal solution. 4. In addition, while `new` and `Drop` make sense for resources, those conventions make less sense for the more abstract idea of a "Test". In most testing frameworks, the idea of a "test" is the combination of some stateful test context initialized `before` the actual test, a test case that can mutate its own context, and some clean-up to be run `after` the actual test. `spekt` avoids all of these issues by providing a `Test` trait that encompasses the `before` -> `test` -> `after` lifecycle of stateful `async` tests that use `Result` to drive assertions. ## How to use: `spekt::Test` can be implemented for any `Send + Sync` test state, enabling a `test()` method that returns a `std::future::Future`. The returned `Future` is runtime-agnostic, and can be evaluated synchronously with `.wait()`, through a per-suite custom runtime (e.g. [`tokio::runtime::Runtime`](https://docs.rs/tokio/0.2.22/tokio/runtime/struct.Runtime.html)), or through an `async` test-runner like [`tokio::test`](https://docs.rs/tokio/0.2.22/tokio/attr.test.html). Rewriting the example above with `spekt::Test`: ``` use tokio_postgres::{Client, NoTls, Row, error::Error as PostgresError}; use spekt::Test; struct PostgresTest { client: Client } // spekt optionally re-exports async_trait #[spekt::async_trait] impl Test for PostgresTest { type Error = anyhow::Error; // any Error will do, but anyhow is recommended async fn before() -> Result<Self, Self::Error> { let mut client = Client::connect("host=localhost user=postgres", NoTls).await?; client.batch_execute("CREATE TABLE my_test_table ()").await?; Ok(Self { client }) } async fn after(&self) -> Result<(), Self::Error> { self.client.batch_execute("DROP TABLE my_test_table")?; Ok(()) } } // any executor will do, but tokio::test is recommended #[tokio::test] async fn adds_queryable_test_table() { // PostgresTest::test runs before(), passing the output to test(), and runs after() regardless // of the result of the test run itself, bubbling all Self::Errors to top-level test failures PostgresTest::test(|context| async move { context.client.query("SELECT FROM my_test_table").await?; Ok(()) }).await } ``` */ #[deny(missing_docs, unreachable_pub)] mod test; pub use self::test::*; pub use async_trait::async_trait;