Using assert_fs and predicates to integration test with a real temporary file system

Chris Biscardi
InstructorChris Biscardi
Share this video with your friends

Social Share Links

Send Tweet

We have been ignoring implementing tests for the write subcommand. Now is the time that we'll test the command we have been building.

We'll build out a fake editor script that our test will use to check if the command is functioning properly. We will then use arg to build the command as we would normally use it ourselves.

After writing the test, we'll see how to clean up after ourselves so that we aren't leaving testing artifacts leftover just from running a test.


How to integration test a cargo binary that calls out to EDITOR for modifications and asks the user for input.

First we'll create a fake editor script to use instead of vim, etc.

tests/fake_editor.sh is a one-liner.

echo "testing" >> $1;

Then we'll write our test using assert_cmd to build up a Command that points to the garden binary in out Cargo.toml.

We grab the fake editor path script using std::env::current_dir(), make sure the file exists (which means we've grabbed the right path), and spawn a new Command with the EDITOR variable set to the fake editor script. The equivalent call to our bin is

EDITOR=path/to/fake-editor.sh garden write -t atitle

And the rust code to execute.

fn test_write_with_title() {
    let mut cmd = Command::cargo_bin("garden").unwrap();
    let fake_editor_path = std::env::current_dir()
        .expect("expect to be in a dir")
        .join("tests")
        .join("fake-editor.sh");
    if !fake_editor_path.exists() {
        panic!(
            "fake editor shell script could not be found"
        )
    }

    let assert = cmd
        .env("EDITOR", fake_editor_path.into_os_string())
        .arg("write")
        .arg("-t")
        .arg("atitle")
        .write_stdin("N\n".as_bytes())
        .assert();

    assert.success();
}

Note that we're asserting that the command exited sucessfully, and that we're inputing N\n to the bin's stdin, which responds to a request for user input after the editor closes.

Note that this doesn't actually test for the resulting file existence, and also that we're writing our files out directly into our default digital garden! We can solve this with assert_fs and predicates.

We'll bring in both preludes (a prelude is a bunch of stuff the library author thinks will be useful to have around all the time) and create a new temporary directory.

We'll use the GARDEN_PATH environment variable to pass the temporary directory in to the garden bin and that's it, we've added a temporary directory to our test. No more polluting the default garden!

use assert_fs::prelude::*;
use predicates::prelude::*;

fn test_write_with_title() {
    let temp_dir = assert_fs::TempDir::new().unwrap();

    let mut cmd = Command::cargo_bin("garden").unwrap();
    let fake_editor_path = std::env::current_dir()
        .expect("expect to be in a dir")
        .join("tests")
        .join("fake-editor.sh");
    if !fake_editor_path.exists() {
        panic!(
            "fake editor shell script could not be found"
        )
    }

    let assert = cmd
        .env("EDITOR", fake_editor_path.into_os_string())
        .env("GARDEN_PATH", temp_dir.path())
        .arg("write")
        .arg("-t")
        .arg("atitle")
        .write_stdin("N\n".as_bytes())
        .assert();

    assert.success();

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

Now that's enough, but we can also check to make sure the file we expect to exist actually exists. temp_dir.child gives us the possible file, and we can assert on that file with a test from predicates that tests to make sure the file exists.

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

Instructor: [0:01] If we go back to our integration desk, we'll remember that two of them are passing, and we ignored one early on that was meant to test whether the right command actually worked. We're going to write this test now.

[0:14] We can get rid of ignore, and we'll see it fail on the right. In this case, the failure mode is that it just waits for user input forever which is unfortunate. We'll kill the watch process so that doesn't happen anymore. We'll test manually for the moment.

[0:37] The first thing we want to do is create a fake editor script. We'll call it fake-editor.sh. What this is, is the program that we're going to execute as the editor.

[0:51] In our command line usage, whenever we test it manually, we've been using EDITOR="code -w". This program is going to be that editor. In this case, all it's going to do is echo testing to whatever file name it gets. These two arrows mean that it will concatenate to whatever is already in the file, which means that our template is preserved as well.

[1:19] Then, we'll write our test using assert_cmd to build up a Command that points to the garden binary in our Cargo.toml. We'll get rid of assert for the moment and add in a way to get the fake editor path script. In this case, we can use the current_dir. We'll expect that we can get the current_dir because this returns a result. If we can't, this test will fail.

[1:48] We'll join tests because current_dir is the root of our package. We'll join with fake-editor.sh. This gives us a path buffer, which we can use to check to see if the file exists. This gives us an early indication about whether we got the path or the fake editor script correct or not. If it's not correct, we'll panic and say, "Hey, you know, we couldn't find the fake editor script."

[2:16] Additionally, we'll take the mutable command that we have, which is calling our garden bin. We'll add the editor environment variable with the fake_editor_path. We turn the fake_editor_path.into_os_string and place it into the environment. Then, we add an argument of write, a -t for the title flag, and an argument, atitle. This is the same as writing -t atitle on our command line.

[2:48] Because we already have a title, the only thing that we'll need to do here is write the standard N\n and an Enter key which is a new line here, .write_stdin takes bytes, so we'll turn our string slice into bytes. That will respond to our prompt for us when it asks us for input saying, "No, this title is OK." We used .assert() to give us the assert so that we can say, "Hey, this binary exited successfully."

[3:21] Now, if we cargo test, we'll see something unfortunate. This is not a good error message for what's going on.

[3:33] Because I know the internals of the libraries that we've used, I know that nano is one of the default platform choices for macOS for the edit package that we used earlier. That means it's failing to use our fake editor script.

[3:47] In this case, it's because our fake editor shell script doesn't have executable permissions, which we can give it with a chmod +x and then a path to the fake editor script. Now, when we test, we can see that the test passes.

[4:04] We do have a problem here, though, that if we look in our garden for atitle.md, for some reason, it's there. No matter how many times we run this test, we'll keep getting more and more atitles with increasing numbers. You can see that this first atitle doesn't have any content, which is the one that didn't work, whereas atitle 5 is a successful test.

[4:32] There's a couple of problems here. One is that we're making our default garden directory pretty dirty, just by running a couple of tests. We don't want to do that because we don't want to mess with it. We want to be able to run our tests on the same system that we're using the CLion.

[4:49] The other problem here is that we're not testing for the file existence, so we don't know if this file's successfully got written out. All we know from this test is that the command did an error when it ran. We can solve this with assert_fs and predicates.

[5:08] Assert_fs will let us create fake files and fake directories that we can run our test in while interacting with a real file system. Predicates will help us to test if the files that we expect to exist actually do exist or not.

[5:22] Change your test to use a temporary directory somewhere on our file system. We need to use a assert_fs::TempDir::new which will give us the temp_dir. Then, we also need to add the environment variable for the GARDEN_PATH to point to that temp_dir, which we can do with temp_dir.path().

[5:44] Finally, we want to use temp_dir.child() with the file name that we were looking for to look for this file name inside of the temporary directory. We don't know where this temporary directory is, so we have to access this using .child() of the temp_dir. We can also assert that the path exists using the predicate from the predicates crate.

[6:07] Note that for these, to both work, we have to bring in the preludes. A prelude is an import from a library that the library author thinks is going to be useful to have in scope all the time.

[6:20] Both of these preludes include a bunch of functions and other traits, and things like that, that the library authors think you'll want when you write tests with these libraries. The star means that we're importing the entire prelude, which is all of the exports.

[6:36] Now, when we run cargo test, we'll download the packages and we can see that our tests pass. If we look into our garden now, before we run the tests, we can see there's no atitle files. If we look after we run the tests, we can see that there's also no atitle files, which means that we're successfully using the temporary directory to write our files into.