#testing #future #async #test-framework

spekt

基于 std::future::Future 和 Result 的测试特质,用于管理具有状态、异步测试的生命周期

2 个版本

0.1.1 2021 年 1 月 26 日
0.1.0 2020 年 8 月 15 日

#1052 in 异步

MIT 许可证

13KB

spekt

A std::future::FutureResult-based testing trait for managing the lifecycle of stateful, asynchronous tests.

为什么选择 spekt

大多数 Rust 单元测试基于 panic 终止(通常由 assert! 触发),通过手动实现 Drop 特质进行资源清理。与数据库等有状态的资源同步工作可能如下所示

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");
}

虽然这对于许多情况都有效,但这个建议存在一些问题

  1. 技术上,Rust 不 保证 Drop 会被执行,并且不应该依赖 Drop 在所有情况下都会被执行。[链接](http://cglab.ca/%7Eabeinges/blah/everyone-poops/)
  2. Drop 也不能是异步的!关于 异步析构函数 已有很多讨论,但至今还没有为 async 函数出现可靠的析构函数特质。
  3. 基于 panic 的断言(及其关联的展开)的行为可能在运行时是不可预测的。[链接](https://github.com/tokio-rs/tokio/issues/2002)。这是一个特定的测试问题,目前还没有好的通用解决方案。[链接](https://github.com/tokio-rs/tokio/issues/2699)
  4. 此外,虽然 newDrop 对于资源来说是有意义的,但这些约定对于更抽象的“测试”概念来说就不那么合理了。在大多数测试框架中,“测试”的概念是初始化在实际测试之前的某些有状态的测试上下文,可以修改自己的上下文的测试用例,以及在实际测试之后运行的某些清理操作。

spekt 通过提供包含 before -> test -> after 生命周期、使用 Result 驱动断言的具有状态、异步测试的 Test 特质,避免了所有这些问题。

如何使用

spekt::Test 可以用于任何 Send + Sync 测试状态,实现一个返回 std::future::Futuretest() 方法。返回的 Future 是运行时无关的,可以通过 .wait() 同步评估,通过每个测试套件的定制运行时(例如 tokio::runtime::Runtime),或者通过异步测试运行器(例如 tokio::test)。

使用 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() first, passes the output of before() to test() as context,
    // and finally 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
}

路线图

依赖项

~280–740KB
~18K SLoC