Parameterize Tests in Rust: rstest

This post will discuss how to parameterize tests in Rust. Qxf2 likes the fact that Rust makes testing a first class citizen of the language. It provides a test runner out of the box. However, coming from Python land, we missed one feature – test parametrization. So went about looking for crates that could help. We have found rstest to be a good framework to start with to parameterize our tests initially.

Problem Statement

We were happily practicing writing some unit tests for key-value store using SQLite. Qxf2 is in no way involved with that project – we just used it for practice since it seemed new and changing. We chose to write a test for creating a table with invalid names. The test structure would look like something below

fn test_create_table_invalid_names() -> Result<(), Error>{
    let table_name = "Something invalid here"
    let connection = Connection::open_in_memory()?;
    let table = Table::new(table_name);
    let result = table.create(&connection);
    assert_eq!(result.is_err(), expected_output, "{}", explanation);
    Ok(())
}

The above is a function from our unit test where we were creating table names with invalid names. Being testers we could imagine several examples of invalid names. We obviously do not want to write one test function for each invalid name. So we thought of parameterizing the unit tests.

Looking up online, we didn’t find any good articles (written in Mar 2023) showing us how to parameterize tests in Rust. So, we thought to do the groundwork ourselves and share it with the larger group.

Install rstest

To use rstest, we should either install or add it in Cargo.toml

To install, Run the following Cargo command in your project directory:

cargo add rstest

OR

Add the following line to your Cargo.toml:

rstest = "0.17.0"

Once installed, add the following to your test file.

use rstest::rstest;

How to use rstest in your tests

For folks that want to get to the meat of the matter, this is how you use rstest. You add an #[rstest] attribute and list the cases you want.

#[rstest(
table_name, expected_output, explanation,
case::special_character("@@@@@#####%%%&&&&", true, "Invalid table name with special characters"),
case::space("table name with spaces", true, "Invalid table name with space"),
case::end_with_special_character("table_name_with_special_characters_#&%", true, "Invalid table name ending with special characters"),
case::start_with_special_character("@#$%^&starts_with_special_character", true, "Invalid table name starting with special characters")
)]

This is how rstest can be used to use various cases for your tests. In our case, we were trying to pick some cases for invalid database table names.

The Test Function for creating a table with Invalid name

The complete code snippet looks something like this. Note that the test function signature has changed to have three new arguments -> table_name, expected_output and explanation. These map to the three arguments within case::blah(). We can now use whatever we had listed within case::blah() as variables within out test.

One point to note is that we like the format case::(args) because when we run cargo test, the test output is much nicer to read.

#[rstest(
table_name, expected_output, explanation,
case::special_character("@@@@@#####%%%&&&&", true, "Invalid table name with special characters"),
case::space("table name with spaces", true, "Invalid table name with space"),
case::end_with_special_character("table_name_with_special_characters_#&%", true, "Invalid table name ending with special characters"),
case::start_with_special_character("@#$%^&starts_with_special_character", true, "Invalid table name starting with special characters")
)]
fn test_create_table_invalid_names(table_name: &str, expected_output: bool, explanation: &str) -> Result<(), Error> {
    let connection = Connection::open_in_memory()?;
    let table = Table::new(table_name);
    let result = table.create(&connection);
    assert_eq!(result.is_err(), expected_output, "{}", explanation);
    Ok(())
}

Decode the test

For folks completely new to Rust, here is a short explanation. In the above test, we tried to do the following:
1. Open a SQLite database connection in memory
2. Create a new table in the database
3. The new table takes the table name from the cases provided within rstest case
4. Create a connection with the newly created table
5. Assert to check that the create() method does raise an error when provided an invalid table name

Hire Qxf2 testers for your next Rust project

Do you have Rust projects that need testers? Qxf2 has the technical testers you are looking for. Our testers go well beyond traditional test automation. We are engineers who test well. Reach out to us here.


Leave a Reply

Your email address will not be published. Required fields are marked *