8000 libcnb-test: Overhaul the README and crate docs by edmorley · Pull Request #478 · heroku/libcnb.rs · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

libcnb-test: Overhaul the README and crate docs #478

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

8000

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions examples/execd/tests/integration_test.rs
10000
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ fn basic() {
TestRunner::default().build(
BuildConfig::new("heroku/builder:22", "test-fixtures/empty-app"),
|context| {
let log_output = context.run_shell_command("env");
assert_empty!(log_output.stderr);
assert_contains!(log_output.stdout, "ROLL_1D6=");
assert_contains!(log_output.stdout, "ROLL_4D6=");
assert_contains!(log_output.stdout, "ROLL_1D20=");
let command_output = context.run_shell_command("env");
assert_empty!(command_output.stderr);
assert_contains!(command_output.stdout, "ROLL_1D6=");
assert_contains!(command_output.stdout, "ROLL_4D6=");
assert_contains!(command_output.stdout, "ROLL_1D20=");
},
);
}
1 change: 1 addition & 0 deletions libcnb-test/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## [Unreleased]

- Overhaul the crate README/docs, to improve the learning/onboarding UX. ([#478](https://github.com/heroku/libcnb.rs/pull/478))
- Rename `TestRunner::run_test` to `TestRunner::build`, `TestConfig` to `BuildConfig` and `TestContext::run_test` to `TestContext::rebuild`. ([#470](https://github.com/heroku/libcnb.rs/pull/470))
- Add `TestContext::start_container`, `TestContext::run_shell_command` and `ContainerConfig`. ([#469](https://github.com/heroku/libcnb.rs/pull/469))
- Remove `TestContext::prepare_container` and `PrepareContainerContext`. To start a container use `TestContext::start_container` combined with `ContainerConfig` (or else the convenience function `TestContext::run_shell_command`) instead. ([#469](https://github.com/heroku/libcnb.rs/pull/469))
Expand Down
224 changes: 193 additions & 31 deletions libcnb-test/README.md
Original file line number Diff line number Diff line change
@@ -1,62 +1,224 @@
# libcnb-test   [![Docs]][docs.rs] [![Latest Version]][crates.io] [![MSRV]][install-rust]

An experimental integration testing framework for Cloud Native Buildpacks written in Rust with libcnb.rs.
An integration testing framework for Cloud Native Buildpacks written in Rust with [libcnb.rs](https://github.com/heroku/libcnb.rs).

## Experimental
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!`)

This crate is marked as experimental. It currently implements the most basic building blocks for writing
integration tests with libcnb.rs. Its feature set is deliberately cut down to get ball rolling and get a better feel
which features are required. See [issues tagged with `libcnb-test`][libcnb-test-label] for possible future improvements.
Please use the same tag for feature requests.
## Dependencies

[libcnb-test-label]: https://github.com/heroku/libcnb.rs/labels/libcnb-test
Integration tests require the following to be available on the host:

## Example
- [Docker](https://docs.docker.com/engine/install/)
- [Pack CLI](https://buildpacks.io/docs/install-pack/)
- [Cross-compilation prerequisites](https://docs.rs/libcnb/latest/libcnb/#cross-compilation-prerequisites) (however `libcnb-cargo` itself is not required)

Only local Docker daemons are fully supported. As such, if you are using Circle CI you must use the
[`machine` executor](https://circleci.com/docs/2.0/executor-types/#using-machine) rather than the
[remote docker](https://circleci.com/docs/2.0/building-docker-images/) 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,no_run
// In $CRATE_ROOT/tests/integration_test.rs
use libcnb_test::{assert_contains, BuildConfig, ContainerConfig, TestRunner};
use libcnb_test::{assert_contains, assert_empty, BuildConfig, TestRunner};

// In your code you'll want to mark your function as a test with `#[test]`.
// It is removed here for compatibility with doctest so this code in the readme
// tests for compilation.
fn test() {
// 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| {
assert_contains!(context.pack_stdout, "---> Maven Buildpack");
assert_contains!(context.pack_stdout, "---> Installing Maven");
assert_contains!(context.pack_stdout, "---> Running mvn package");
assert_empty!(context.pack_stderr);
assert_contains!(context.pack_stdout, "Expected build output");
},
);
}
```

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

```rust,no_run
use libcnb_test::{assert_contains, BuildConfig, TestRunner};

// #[test]
fn rebuild() {
TestRunner::default().build(
BuildConfig::new("heroku/builder:22", "test-fixtures/app"),
|context| {
assert_contains!(context.pack_stdout, "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,no_run
use libcnb_test::{assert_contains, BuildConfig, PackResult, TestRunner};

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

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

```rust,no_run
use libcnb_test::{assert_empty, BuildConfig, TestRunner};

// #[test]
fn run_shell_command() {
TestRunner::default().build(
BuildConfig::new("heroku/builder:22", "test-fixtures/app"),
|context| {
// ...
let command_output = context.run_shell_command("python --version");
assert_empty!(command_output.stderr);
assert_eq!(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,no_run
use libcnb_test::{assert_contains, assert_empty, BuildConfig, ContainerConfig, TestRunner};
use std::thread;
use std::time::Duration;

const TEST_PORT: u16 = 12345;

// #[test]
fn starting_web_server_container() {
TestRunner::default().build(
BuildConfig::new("heroku/builder:22", "test-fixtures/app"),
|context| {
// ...
context.start_container(
ContainerConfig::new()
.env("PORT", "12345")
.expose_port(12345),
.env("PORT", TEST_PORT.to_string())
.expose_port(TEST_PORT),
|container| {
assert_eq!(
call_test_fixture_service(
container.address_for_port(12345).unwrap(),
"Hagbard Celine"
)
.unwrap(),
"enileC drabgaH"
let address_on_host = container.address_for_port(TEST_PORT).unwrap();
let url = format!("http://{}:{}", address_on_host.ip(), address_on_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,no_run
use libcnb_test::{assert_contains, BuildConfig, ContainerConfig, TestRunner};

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

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

```rust,no_run
use libcnb_test::{BuildConfig, TestRunner};
use std::fs;

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

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

```rust,no_run
use libcnb_test::{BuildConfig, BuildpackReference, TestRunner};

fn call_test_fixture_service(addr: std::net::SocketAddr, payload: &str) -> Result<String, ()> {
unimplemented!()
// #[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| {
// ...
},
);
}
```

## Known issues
## Tips

- Only local Docker daemons are fully supported. If using Circle CI you must use the
[`machine` executor](https://circleci.com/docs/2.0/executor-types/#using-machine) rather
than the [remote docker](https://circleci.com/docs/2.0/building-docker-images/) feature.
- Rust tests are automatically run in parallel, however only if they are in the same crate.
For integration tests Rust compiles each file as a separate crate. As such, make sure to
include all integration tests in a single file (either inlined or by including additional
test modules) to ensure they run in parallel.
- If you would like to be able to more easily run your unit tests and integration tests
separately, annotate each integration test with `#[ignore = "integration test"]`, which
causes `cargo test` to skip them (running unit/doc tests only). The integration tests
can then be run using `cargo test -- --ignored`, or all tests can be run at once using
`cargo test -- --include-ignored`.
- If you wish to assert against multi-line log output, see the [indoc](https://crates.io/crates/indoc) crate.

[Docs]: https://img.shields.io/docsrs/libcnb-test
[docs.rs]: https://docs.rs/libcnb-test/latest/libcnb_test/
Expand Down
10 changes: 5 additions & 5 deletions libcnb-test/src/build_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub struct BuildConfig {
}

impl BuildConfig {
/// Creates a new test configuration.
/// Creates a new build configuration.
///
/// If the `app_dir` parameter is a relative path, it is treated as relative to the Cargo
/// manifest directory ([`CARGO_MANIFEST_DIR`](https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates)),
Expand Down Expand Up @@ -48,7 +48,7 @@ impl BuildConfig {
}
}

/// Sets the buildpacks order.
/// Sets the buildpacks (and their ordering) to use when building the app.
///
/// Defaults to [`BuildpackReference::Crate`].
///
Expand Down Expand Up @@ -220,21 +220,21 @@ impl BuildConfig {

/// Set the expected `pack` command result.
///
/// In some cases, users might want to explicitly test that a build fails and asserting against
/// In some cases, users might want to explicitly test that a build fails and assert against
/// error output. When passed [`PackResult::Failure`], the test will fail if the pack build
/// succeeds and vice-versa.
///
/// Defaults to [`PackResult::Success`].
///
/// # Example
/// ```no_run
/// use libcnb_test::{BuildConfig, PackResult, TestRunner};
/// use libcnb_test::{assert_contains, BuildConfig, PackResult, TestRunner};
///
/// TestRunner::default().build(
/// BuildConfig::new("heroku/builder:22", "test-fixtures/app")
/// .expected_pack_result(PackResult::Failure),
/// |context| {
/// // ...
/// assert_contains!(context.pack_stderr, "ERROR: Invalid Procfile!");
/// },
/// );
/// ```
Expand Down
8 changes: 4 additions & 4 deletions libcnb-test/src/test_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ impl<'a> TestContext<'a> {
/// BuildConfig::new("heroku/builder:22", "test-fixtures/app"),
/// |context| {
/// // ...
/// let log_output = context.run_shell_command("for i in {1..3}; do echo \"${i}\"; done");
/// assert_eq!(log_output.stdout, "1\n2\n3\n");
/// let command_output = context.run_shell_command("for i in {1..3}; do echo \"${i}\"; done");
/// assert_eq!(command_output.stdout, "1\n2\n3\n");
/// },
/// );
/// ```
Expand Down Expand Up @@ -184,11 +184,11 @@ impl<'a> TestContext<'a> {
/// TestRunner::default().build(
/// BuildConfig::new("heroku/builder:22", "test-fixtures/app"),
/// |context| {
/// assert_contains!(context.pack_stdout, "---> Installing gems");
/// assert_contains!(context.pack_stdout, "---> Installing dependencies");
///
/// let config = context.config.clone();
/// context.rebuild(config, |context| {
/// assert_contains!(context.pack_stdout, "---> Using cached gems");
/// assert_contains!(context.pack_stdout, "---> Using cached dependencies");
/// });
/// },
/// );
Expand Down
Loading
0