libcnb-test   ![Docs] ![Latest Version] ![MSRV]

An integration testing framework for Cloud Native Buildpacks written in Rust with libcnb.rs.

The framework: - Automatically cross-compiles and packages the buildpack under test - Performs a build with specified configuration using pack build - Supports starting containers using the resultant application image - Supports concurrent test execution - Handles cleanup of the test containers and images - Provides additional test assertion macros to simplify common test scenarios (for example, assert_contains!)

Dependencies

Integration tests require the following to be available on the host:

Only local Docker daemons are fully supported. As such, if you are using Circle CI you must use the machine executor rather than the remote docker feature.

Examples

A basic test that performs a build with the specified builder image and app source fixture, and then asserts against the resultant pack build log output:

```rust,norun // In $CRATEROOT/tests/integrationtest.rs use libcnbtest::{assertcontains, assertempty, BuildConfig, TestRunner};

// Note: In your code you'll want to uncomment the #[test] annotation here. // It's commented out in these examples so that this documentation can be // run as a doctest and so checked for correctness in CI. // #[test] fn basic() { TestRunner::default().build( BuildConfig::new("heroku/builder:22", "test-fixtures/app"), |context| { assertempty!(context.packstderr); assertcontains!(context.packstdout, "Expected build output"); }, ); } ```

Performing a second build of the same image to test cache handling, using [TestContext::rebuild]:

```rust,norun use libcnbtest::{assert_contains, BuildConfig, TestRunner};

// #[test] fn rebuild() { TestRunner::default().build( BuildConfig::new("heroku/builder:22", "test-fixtures/app"), |context| { assertcontains!(context.packstdout, "Installing dependencies");

        let config = context.config.clone();
        context.rebuild(config, |rebuild_context| {
            assert_contains!(rebuild_context.pack_stdout, "Using cached dependencies");
        });
    },
);

} ```

Testing expected buildpack failures, using [BuildConfig::expected_pack_result]:

```rust,norun use libcnbtest::{assert_contains, BuildConfig, PackResult, TestRunner};

// #[test] fn expectedpackfailure() { TestRunner::default().build( BuildConfig::new("heroku/builder:22", "test-fixtures/invalid-app") .expectedpackresult(PackResult::Failure), |context| { assertcontains!(context.packstderr, "ERROR: Invalid Procfile!"); }, ); } ```

Running a shell command against the built image, using [TestContext::run_shell_command]:

```rust,norun use libcnbtest::{assert_empty, BuildConfig, TestRunner};

// #[test] fn runshellcommand() { TestRunner::default().build( BuildConfig::new("heroku/builder:22", "test-fixtures/app"), |context| { // ... let commandoutput = context.runshellcommand("python --version"); assertempty!(commandoutput.stderr); asserteq!(command_output.stdout, "Python 3.10.4\n"); }, ); } ```

Starting a container using the default process with an exposed port to test a web server, using [TestContext::start_container]:

```rust,norun use libcnbtest::{assertcontains, assertempty, BuildConfig, ContainerConfig, TestRunner}; use std::thread; use std::time::Duration;

const TEST_PORT: u16 = 12345;

// #[test] fn startingwebservercontainer() { TestRunner::default().build( BuildConfig::new("heroku/builder:22", "test-fixtures/app"), |context| { // ... context.startcontainer( ContainerConfig::new() .env("PORT", TESTPORT.tostring()) .exposeport(TESTPORT), |container| { let addressonhost = container.addressforport(TESTPORT).unwrap(); let url = format!("http://{}:{}", addressonhost.ip(), addresson_host.port());

                // Give the server time to start.
                thread::sleep(Duration::from_secs(2));

                let server_log_output = container.logs_now();
                assert_empty!(server_log_output.stderr);
                assert_contains!(
                    server_log_output.stdout,
                    &format!("Listening on port {TEST_PORT}")
                );

                let response = ureq::get(&url).call().unwrap();
                let body = response.into_string().unwrap();
                assert_contains!(body, "Expected response substring");
            },
        );
    },
);

} ```

Inspecting an already running container using Docker Exec, using [ContainerContext::shell_exec]:

```rust,norun use libcnbtest::{assert_contains, BuildConfig, ContainerConfig, TestRunner};

// #[test] fn shellexec() { TestRunner::default().build( BuildConfig::new("heroku/builder:22", "test-fixtures/app"), |context| { // ... context.startcontainer(ContainerConfig::new(), |container| { // ... let execlogoutput = container.shellexec("ps"); assertcontains!(execlogoutput.stdout, "nginx"); }); }, ); } ```

Dynamically modifying test fixtures during test setup, using [BuildConfig::app_dir_preprocessor]:

```rust,norun use libcnbtest::{BuildConfig, TestRunner}; use std::fs;

// #[test] fn dynamicfixture() { TestRunner::default().build( BuildConfig::new("heroku/builder:22", "test-fixtures/app").appdirpreprocessor( |appdir| { fs::write(app_dir.join("runtime.txt"), "python-3.10").unwrap(); }, ), |context| { // ... }, ); } ```

Building with multiple buildpacks, using [BuildConfig::buildpacks]:

```rust,norun use libcnbtest::{BuildConfig, BuildpackReference, TestRunner};

// #[test] fn additional_buildpacks() { TestRunner::default().build( BuildConfig::new("heroku/builder:22", "test-fixtures/app").buildpacks(vec![ BuildpackReference::Crate, BuildpackReference::Other(String::from("heroku/another-buildpack")), ]), |context| { // ... }, ); } ```

Tips