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