Expand description

testing Utilities for writing PyO3 Asyncio tests

PyO3 Asyncio Testing Utilities

This module provides some utilities for parsing test arguments as well as running and filtering a sequence of tests.

As mentioned here, PyO3 Asyncio tests cannot use the default test harness since it doesn’t allow Python to gain control over the main thread. Instead, we have to provide our own test harness in order to create integration tests.

Running pyo3-asyncio code in doc tests is supported however since each doc test has its own main function. When writing doc tests, you may use the #[pyo3_asyncio::async_std::main] or #[pyo3_asyncio::tokio::main] macros on the test’s main function to run your test.

If you don’t want to write doc tests, you’re unfortunately stuck with integration tests since lib tests do not offer the same level of flexibility for the main fn. That being said, overriding the default test harness can be quite different from what you’re used to doing for integration tests, so these next sections will walk you through this process.

Main Test File

First, we need to create the test’s main file. Although these tests are considered integration tests, we cannot put them in the tests directory since that is a special directory owned by Cargo. Instead, we put our tests in a pytests directory.

The name pytests is just a convention. You can name this folder anything you want in your own projects.

We’ll also want to provide the test’s main function. Most of the functionality that the test harness needs is packed in the pyo3_asyncio::testing::main function. This function will parse the test’s CLI arguments, collect and pass the functions marked with #[pyo3_asyncio::async_std::test] or #[pyo3_asyncio::tokio::test] and pass them into the test harness for running and filtering.

pytests/test_example.rs for the tokio runtime:

#[pyo3_asyncio::tokio::main]
async fn main() -> pyo3::PyResult<()> {
    pyo3_asyncio::testing::main().await
}

pytests/test_example.rs for the async-std runtime:

#[pyo3_asyncio::async_std::main]
async fn main() -> pyo3::PyResult<()> {
    pyo3_asyncio::testing::main().await
}

Cargo Configuration

Next, we need to add our test file to the Cargo manifest by adding the following section to the Cargo.toml

[[test]]
name = "test_example"
path = "pytests/test_example.rs"
harness = false

Also add the testing and attributes features to the pyo3-asyncio dependency and select your preferred runtime:

pyo3-asyncio = { version = "0.13", features = ["testing", "attributes", "async-std-runtime"] }

At this point, you should be able to run the test via cargo test

Adding Tests to the PyO3 Asyncio Test Harness

We can add tests anywhere in the test crate with the runtime’s corresponding #[test] attribute:

For async-std use the pyo3_asyncio::async_std::test attribute:

mod tests {
    use std::{time::Duration, thread};

    use pyo3::prelude::*;

    // tests can be async
    #[pyo3_asyncio::async_std::test]
    async fn test_async_sleep() -> PyResult<()> {
        async_std::task::sleep(Duration::from_secs(1)).await;
        Ok(())
    }

    // they can also be synchronous
    #[pyo3_asyncio::async_std::test]
    fn test_blocking_sleep() -> PyResult<()> {
        thread::sleep(Duration::from_secs(1));
        Ok(())
    }
}

#[pyo3_asyncio::async_std::main]
async fn main() -> pyo3::PyResult<()> {
    pyo3_asyncio::testing::main().await
}

For tokio use the pyo3_asyncio::tokio::test attribute:

mod tests {
    use std::{time::Duration, thread};

    use pyo3::prelude::*;

    // tests can be async
    #[pyo3_asyncio::tokio::test]
    async fn test_async_sleep() -> PyResult<()> {
        tokio::time::sleep(Duration::from_secs(1)).await;
        Ok(())
    }

    // they can also be synchronous
    #[pyo3_asyncio::tokio::test]
    fn test_blocking_sleep() -> PyResult<()> {
        thread::sleep(Duration::from_secs(1));
        Ok(())
    }
}

#[pyo3_asyncio::tokio::main]
async fn main() -> pyo3::PyResult<()> {
    pyo3_asyncio::testing::main().await
}

Lib Tests

Unfortunately, as we mentioned at the beginning, these utilities will only run in integration tests and doc tests. Running lib tests are out of the question since we need control over the main function. You can however perform compilation checks for lib tests. This is much more useful in doc tests than it is for lib tests, but the option is there if you want it.

my-crate/src/lib.rs

mod tests {
    use pyo3::prelude::*;

    #[pyo3_asyncio::async_std::test]
    async fn test_async_std_async_test_compiles() -> PyResult<()> {
        Ok(())
    }
    #[pyo3_asyncio::async_std::test]
    fn test_async_std_sync_test_compiles() -> PyResult<()> {
        Ok(())
    }

    #[pyo3_asyncio::tokio::test]
    async fn test_tokio_async_test_compiles() -> PyResult<()> {
        Ok(())
    }
    #[pyo3_asyncio::tokio::test]
    fn test_tokio_sync_test_compiles() -> PyResult<()> {
        Ok(())
    }
}

Structs

Args that should be provided to the test program

The structure used by the #[test] macros to provide a test to the pyo3-asyncio test harness.

Functions

Parses test arguments and passes the tests to the pyo3-asyncio test harness

Parse the test args from the command line

Run a sequence of tests while applying any necessary filtering from the Args