Abstracting test utilities inside integration test files

Chris Biscardi
InstructorChris Biscardi
Share this video with your friends

Social Share Links

Send Tweet

While we have test_write_with_title which tests the garden CLI with a given title flag, we can also set up additional tests, such as for using a written title in the document for the filename.

Copy/pasting the code from test one into test two is a lot of duplication, and setting up the fake editor isn't going to change across tests, so let's abstract the creation of the temporary directory, the fake editor script path, and the Command itself into a utility function.

The port is relatively straightforward, with the only changes being that we cut off the cmd modifications before the .arg calls, and now have to return the cmd and the temp_dir in a tuple.

We return the actual Command and TempDir, yielding ownership to the calling function because we have no further use for them here, but the calling function will need to mutate the Command more, and use TempDir to test.

fn setup_command() -> (Command, assert_fs::TempDir) {
    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"
        )
    }

    cmd.env("EDITOR", fake_editor_path.into_os_string())
        .env("GARDEN_PATH", temp_dir.path());
    (cmd, temp_dir)
}

The new write tests are considerably shorter now and focus mostly on which subcommand to call, and what the user interaction with that subcommand is.


fn test_write_with_title() {
    let (mut cmd, temp_dir) = setup_command();
    let assert = cmd
        .arg("write")
        .arg("-t")
        .arg("atitle")
        .write_stdin("N\n".as_bytes())
        .assert();

    assert.success();

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

fn test_write_with_written_title() {
    let (mut cmd, temp_dir) = setup_command();
    let assert = cmd
        .arg("write")
        .write_stdin("N\n".as_bytes())
        .assert();

    assert.success();

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

Instructor: [0:00] While we have TestWrite, which tests our application's write subcommand with a given title flag, we can also set up additional tests, such as for using a written title in the document for the file name instead of passing one on the command line.

[0:13] I'm going to copy and paste this entire test and name one TestWrite With Title and name the other TestWrite With Written Title. Both of these tests currently pass, because they're exactly the same code.

[0:27] Let's start by fixing the second test and removing the title flag. This will cause our application to write out a file called testing.md, which is the content of the heading in the file. As a reminder, our fake editor echoes testing to the end of the file, which will result in # testing. We can see that our test is panicking and that we have failed the exists test on the file.

[0:54] In this case, we can rename it to testing.md, and all four of our tests are passing again. This still leaves us with a lot of duplication between these two tests. That's a lot of room for user error. It's also a lot of room for refactoring pain if later we want to change anything about how these slides are handled, or how this fake editor path is going to be inserted.

[1:16] Let's abstract the creation of the temporary directory on line 27, the fake editor script path on line 30, and the command itself on 29, into a utility function. We'll have a function called set-up command that will return a command in a tempter.

[1:34] The port is relatively straightforward. By that what I mean is that we haven't made structural changes to the functionality of any of this code. We still create the tempter. We still create the command. We're getting the fake editor path in the same way.

[1:49] We're checking if it exists. The only real change here is that we've cut off the environment variable definitions from being in our task to being in our helper. That means our editor and garden path will get inserted by this helper instead of by our test itself.

[2:08] To return multiple values for our arrest function, we have to return a tuple. We could also set up a struct, but this return value doesn't deserve a name. We're only using it as a helper. We're going to use a tuple which allows us to put two different type values in the first and second slots.

[2:28] Now, if we use our setup command instead of a bunch of the code that we had there before and we also have removed the environment variable declarations from our SAR, we can still chain our arguments on chain our standard in, and assert on the output.

[2:42] We'll do the same for the other test, again removing the environment variable declarations. That's it. Now we have a helper that we can use to create any number of tests beyond this.

[2:53] As a final note, I'll say that this setup command isn't run as a test itself, because it doesn't have the test attribute like we have on line 25 here. Line five has no attribute, so this is not recognized as a test.