🚀 Mastering Integration Testing in Rust with Testcontainers 🧪
Tags: rust, testing, programming, softwaredevelopment
Published: 2023-09-06 04:11:35

In the vast world of software engineering, testing plays an indispensable role in ensuring the reliability, stability, and performance of our applications. Among various testing types, integration testing stands out as a critical phase, verifying that different components of your system interact seamlessly. However, integration testing, especially when dealing with external dependencies like databases and message brokers, can be complex.

In this article, we will embark on a journey to master integration testing in Rust using a powerful tool known as Testcontainers. This tool simplifies the process of spinning up Docker containers for your integration tests, allowing you to test against real external systems in a clean and isolated environment.

## 🚀 Getting Started: Installing Rust

Before diving into integration testing with Testcontainers, let's ensure that your Rust development environment is set up correctly. Rust, known as "the language of systems," provides a robust foundation for building fast and reliable software.

### **Installing Rust on Windows**

1. Visit the official Rust website [here](https://rustup.rs/).

2. Download the Windows installer and run it.

3. Follow the on-screen instructions to complete the installation.

4. Open Command Prompt or PowerShell and run the following command to verify the installation:

   ```shell
   rustc --version
   ```

   This command should display the Rust version, confirming a successful installation.

### **Installing Rust on macOS**

1. Open Terminal, which comes pre-installed on macOS.

2. Install Rust using Homebrew, a popular package manager for macOS, with this command:

   ```shell
   brew install rust
   ```

3. After the installation completes, verify Rust's installation by running:

   ```shell
   rustc --version
   ```

   You should see the Rust version displayed in the terminal, confirming that Rust is now installed on your macOS system.

## 🏗️ Setting Up the Integration Testing Project

Assuming you have a typical Rust project structure, which includes `src/main.rs` for your application code and `tests/integration_test.rs` for integration tests, we can proceed to set up our integration testing project.

To get started, add the Testcontainers dependency to your `Cargo.toml` file:

```shell
❯ cargo add testcontainers     
    Updating crates.io index
      Adding testcontainers v0.14.0 to dependencies.
             Features:
             - async-trait
             - bollard
             - conquer-once
             - experimental
             - signal-hook
             - tokio
             - watchdog
    Updating crates.io index
```

```toml
[dependencies]
testcontainers = "0.14.0"
```

Now, we're ready to dive into the specifics of integration testing with Testcontainers, using PostgreSQL as example.

### 🌐 Setting Up the Integration Testing Project

Assuming you have a typical Rust project structure, which looks like this:

```
my_rust_project/
|-- src/
|   |-- main.rs
|-- tests/
|   |-- integration_test.rs
```

- `src/main.rs` contains your application code.
- `tests/integration_test.rs` is where we'll write our integration tests.


## 🐳 Leveraging Testcontainers for Rust

Testcontainers provides an intuitive way to manage Docker containers for your integration tests. Let's explore how to use it effectively in Rust.

### **Testing PostgreSQL Integration**

In your `integration_test.rs` file, you can set up and test a PostgreSQL container with the following code:

```rust

...

#[test]
async fn it_works() {
    let docker = clients::Cli::default();

    // Define a PostgreSQL container image
    let postgres_image = Postgres::default();

    let pg_container = docker.run(postgres_image);

    pg_container.start();

    WaitFor::seconds(60);

    // Get the PostgreSQL port
    let pg_port = pg_container.get_host_port_ipv4(5432);

    // Define the connection to the Postgress client
    let (client, connection) = tokio_postgres::Config::new()
        .user("postgres")
        .password("postgres")
        .host("localhost")
        .port(pg_port)
        .dbname("postgres")
        .connect(tokio_postgres::NoTls)
        .await
        .unwrap();

    // Spawn connection
    tokio::spawn(async move {
        if let Err(error) = connection.await {
            eprintln!("Connection error: {}", error);
        }
    });

    let _ = client
        .batch_execute(
            "
        CREATE TABLE IF NOT EXISTS app_user (
            id              SERIAL PRIMARY KEY,
            username        VARCHAR UNIQUE NOT NULL,
            password        VARCHAR NOT NULL,
            email           VARCHAR UNIQUE NOT NULL
            )
    ",
        )
        .await;

    let _ = client
        .execute(
            "INSERT INTO app_user (username, password, email) VALUES ($1, $2, $3)",
            &[&"user1", &"mypass", &"user@test.com"],
        )
        .await;

    let result = client
        .query("SELECT id, username, password, email FROM app_user", &[])
        .await
        .unwrap();

    let users: Vec<User> = result.into_iter().map(|row| User::from(row)).collect();

    let user = users.first().unwrap();

    assert_eq!(1, user.id);
    assert_eq!("user1", user.username);
    assert_eq!("mypass", user.password);
    assert_eq!("user@test.com", user.email);
}
```

In this test, we:

1. Create a Docker client.
2. Define a PostgreSQL container image.
3. Run the PostgreSQL container, expose its ports, and set  the user and password.
4. Retrieve the PostgreSQL port and add it to the connection URL.
5. Connect to the PostgresSQL DB using tokio_postgres
6. We need to spawn a new connection
7. Write your database integration test code using the `client`.

## 🏃 Running the Integration Tests

To run your integration tests, use the following command:

```shell
❯ cargo test --test integration_test
   Compiling testcontainer_example v0.1.0 (/Users/sergiomarcial/Documents/Confidential/repositories/github/testcontainers-rust)
    Finished test [unoptimized + debuginfo] target(s) in 1.76s
     Running tests/integration_test.rs (target/debug/deps/integration_test-ff1cb10ac2b24a1b)

running 1 test
test it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.11s
```

This command will execute all the tests in your `integration_test.rs` file.

## Code example

For a complete example using `Rust` and `Postgres` testcontainers, check out my [GitHub repository](https://github.com/sergiommarcial/testcontainers-rust).

## 🔄 Exploring Alternatives and Considerations

While Testcontainers simplifies integration testing, it's crucial to explore alternative approaches like mocking or using Docker Compose for more complex setups. Choose the approach that best aligns with your project's requirements.

## 🎉 Conclusion

Integration testing in Rust can be made effortless and efficient with Testcontainers. You've learned how to set up your project, install dependencies, and write integration tests for PostgreSQL. This newfound knowledge empowers you to ensure that your Rust applications seamlessly interact with external dependencies, bolstering the robustness and reliability of your software. Happy testing!

## 📚 Further Reading

- [Testcontainers Rust Documentation](https://docs.rs/testcontainers/0.10.0/testcontainers/)
- [Rust Programming Language](https://www.rust-lang.org/)

Integration testing has never been this enjoyable! 🐳🦀