DEV Community

Cover image for πŸš€ Mastering Integration Testing in Rust with Testcontainers πŸ§ͺ
Sergio Marcial
Sergio Marcial

Posted on

πŸš€ Mastering Integration Testing in Rust with Testcontainers πŸ§ͺ

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.

  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:

   rustc --version
Enter fullscreen mode Exit fullscreen mode

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:

   brew install rust
Enter fullscreen mode Exit fullscreen mode
  1. After the installation completes, verify Rust's installation by running:
   rustc --version
Enter fullscreen mode Exit fullscreen mode

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:

❯ 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
Enter fullscreen mode Exit fullscreen mode
[dependencies]
testcontainers = "0.14.0"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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:


...

#[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);
}
Enter fullscreen mode Exit fullscreen mode

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:

❯ 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
Enter fullscreen mode Exit fullscreen mode

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.

πŸ”„ 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

Integration testing has never been this enjoyable! πŸ³πŸ¦€

Top comments (0)