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
}
```
*/
pub use *;
pub use async_trait;