Lesson Details

Let’s build out more tests.

To start out let’s pull out the section of code in our interactive test that sets up the temp_dir and command.

Bring TempDir into scope.

use assert_fs::{prelude::*, TempDir};

We define a setup_command function that returns a Result with any error in the Err variant. The Ok variant will return both our bootstrapped Command.

#[cfg(not(target_os = "windows"))]
fn setup_command(
) -> Result<(Command, TempDir), Box<dyn Error>> {
    let temp_dir = assert_fs::TempDir::new()?;

    let bin_path = assert_cmd::cargo::cargo_bin("garden");
    let fake_editor_path = std::env::current_dir()?
        .join("tests")
        .join("fake-editor.sh");
    if !fake_editor_path.exists() {
        panic!(
            "fake editor shell script could not be found"
        )
    }

    let mut cmd = Command::new(bin_path);
    cmd.env(
        "EDITOR",
        fake_editor_path.into_os_string(),
    )
    .env("GARDEN_PATH", temp_dir.path())
    .env("NO_COLOR", "true");

    Ok((cmd, temp_dir))
}

We can then use our setup_command in our interactive test, finishing the Command setup with the arguments we want to pass in.

#[cfg(not(target_os = "windows"))]
#[test]
fn test_write_with_title() -> Result<(), Box<dyn Error>> {
    let (mut cmd, temp_dir) = setup_command()?;

    cmd.arg("write").arg("-t").arg("atitle");

    let mut process = spawn_command(cmd, Some(30000))?;

    process.exp_string("current title: ")?;
    process.exp_string("atitle")?;
    process.exp_regex("\\s*")?;
    process.exp_string(
        "Do you want a different title? (y/N): ",
    )?;
    process.send_line("N")?;
    process.exp_eof()?;

    temp_dir
        .child("atitle.md")
        .assert(predicate::path::exists());
    Ok(())
}

Creating Extension traits

The spawn_command function returns a PtySession, which includes a number of helpful functions for testing the output of our binary…

but what if we want to test the same assertions over and over?

For example, we need to use 4 expectations to verify that a title is what we think it should be, but we’ll want to test to see if a title is accurate in multiple tests.

We can create an extension trait to add more functions to the PtySession type.

Bring the PtySession type into scope.

#[cfg(not(target_os = "windows"))]
use rexpect::session::{spawn_command, PtySession};

An extension trait is just a regular trait that adds some functions to a type when implemented. We’ll call our trait GardenExpectations, and the function that needs to be implemented exp_title.

In our trait definition we only need to focus on typing out what function signature implementors of this trait are going to satisfy.

We know that to call .exp_string, we need an exclusive reference to the PtySession because that’s what the function signature for exp_string says. So we’ll say we need an exclusive reference as well.

We’ll also want a string slice for the title we need to look for.

We’ll also be using rexpect extensively, so we’ll use rexpect::error::Error as our error type.

trait GardenExpectations {
    fn exp_title(
        &mut self,
        title: &str,
    ) -> Result<(), rexpect::error::Error>;
}

We can then implement our GardenExpectations trait for PtySession.

This works much like defining the function regularly, with the same function signature we defined in our trait.

copy/paste the code we already wrote from our test, and use the title instead of a hardcoded value.

impl GardenExpectations for PtySession {
    fn exp_title(
        &mut self,
        title: &str,
    ) -> Result<(), rexpect::error::Error> {
        self.exp_string("current title: ")?;
        self.exp_string(title)?;
        self.exp_regex("\\s*")?;
        self.exp_string(
            "Do you want a different title? (y/N): ",
        )?;
        Ok(())
    }
}

We’re then free to use our exp_title function whenever we have a PtySession.

#[cfg(not(target_os = "windows"))]
#[test]
fn test_write_with_title() -> Result<(), Box<dyn Error>> {
    let (mut cmd, temp_dir) = setup_command()?;

    cmd.arg("write").arg("-t").arg("atitle");

    let mut process = spawn_command(cmd, Some(30000))?;

    process.exp_title("atitle")?;
    process.send_line("N")?;
    process.exp_eof()?;

    temp_dir
        .child("atitle.md")
        .assert(predicate::path::exists());
    Ok(())
}

and we can now continue to write additional tests that use our new setup_command and exp_title.

#[cfg(not(target_os = "windows"))]
#[test]
fn test_write_with_written_title(
) -> Result<(), Box<dyn Error>> {
    let (mut cmd, temp_dir) = setup_command()?;
    cmd.arg("write");

    let mut process = spawn_command(cmd, Some(30000))?;

    process.exp_title("testing")?;
    process.send_line("N")?;
    process.exp_eof()?;

    temp_dir
        .child("testing.md")
        .assert(predicate::path::exists());
    Ok(())
}

and that’s it! We’ve built and tested an interactive CLI in Rust!

❯ cargo test
    Finished test [unoptimized + debuginfo] target(s) in 0.04s
     Running unittests src/lib.rs (target/debug/deps/garden-32ef13859f30a58d)

running 3 tests
test tests::title_from_content_no_title ... ok
test tests::title_from_content_string ... ok
test tests::title_from_empty_string ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/debug/deps/garden-3738e58180b4ff04)

running 0 tests

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

     Running tests/integration.rs (target/debug/deps/integration-0a0ea84a86a8f7b7)

running 4 tests
test test_write_help ... ok
test test_help ... ok
test test_write_with_written_title ... ok
test test_write_with_title ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.21s

   Doc-tests garden

running 0 tests

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