Let's add some integration tests

Good to know

Implementation

Add a new folder named tests beside to src.

.
β”œβ”€β”€ Cargo.lock
β”œβ”€β”€ Cargo.toml
β”œβ”€β”€ README.md
β”œβ”€β”€ src
β”œβ”€β”€ target
└── tests

Add tests/test_utils.rs file to have a place for common test utilities. Although there are some debates what is the best way to share utilities between tests, I found this way simple and effective. Add your utilities to tests/test_utilities.rs and use test_utilities; in a test file you need them. Talk is cheap, let's write the code:

We need a function to create a directory manager for us, but we need to create a random name for the work_tree directory. That's why we're going to use uuid crate. Either add it manually to your Cargo.toml or run cargo add uuid --features=v4

Now let's create a DirectoryManager object with a randomly generated temp path for its base directory.

// tests/test_utilities.rs

#[cfg(test)]
pub mod directory_manager {
    use rit::DirectoryManager;

    use super::general::generate_random_path;
    pub fn create_directory_manager() -> DirectoryManager {
        rit::DirectoryManager::new(generate_random_path())
    }
}

#[cfg(test)]
pub mod general {
    use std::path::PathBuf;

    pub fn generate_random_path() -> PathBuf {
        std::env::temp_dir().join(uuid::Uuid::new_v4().to_string())
    }
}

OK! We're ready to add our first integration tests. Why didn't we add them as unit tests? Because we're going to play with filesystem to see if it works in wild!

Add a new file tests/directory_manager.rs. Add a test to see what happens if we call create_directory_tree. Does it really create some folders for us?

// To be able to use test_utils. Each file in tests directory will become a separate crate/executable.
mod test_utils;

use test_utils::directory_manager::create_directory_manager;

#[test]
fn directory_tree_should_be_created_successfully() {
    let dir_manager = create_directory_manager();

    dir_manager.create_directory_tree().unwrap();

    assert!(dir_manager.dot_git_path.exists());
    assert!(dir_manager.branches_path.exists());
    assert!(dir_manager.objects_path.exists());
    assert!(dir_manager.refs_heads_path.exists());
    assert!(dir_manager.refs_tags_path.exists());
}

Next test! If we have an empty base_directory, is_dot_git_empty should happily return true. Because we don't .git

#[test]
fn if_work_tree_directory_is_empty_is_dot_git_empty_should_return_true() {
    let dir_manager = create_directory_manager();
    std::fs::create_dir_all(&dir_manager.work_tree).unwrap();

    assert!(dir_manager.is_dot_git_empty().unwrap());
}

If .git exists but it's empty, is_dot_git_empty should return true too.

#[test]
fn if_dot_git_is_empty_is_dot_git_empty_should_return_true() {
    let dir_manager = DirectoryManager::new(temp_dir().join(Uuid::new_v4().to_string()));

    // To create only .git file. Nothing inside it!
    std::fs::create_dir_all(&dir_manager.dot_git_path).unwrap();

    assert!(dir_manager.is_dot_git_empty().unwrap());
}

And finally if .git is not empty, is_dot_git_empty should return false:

#[test]
fn if_dot_git_is_not_empty_is_dot_git_empty_should_return_false() {
    let dir_manager = create_directory_manager();

    // To create .git folder and all of its children.
    dir_manager.create_directory_tree().unwrap();

    assert!(!dir_manager.is_dot_git_empty().unwrap());
}

Run them with cargo test. Failures! Let's make these tests happy.

// src/directory_manager.rs
pub struct DirectoryManager {
    // ...
    // Add some new fields.
    pub branches_path: PathBuf,
    pub objects_path: PathBuf,
    pub refs_tags_path: PathBuf,
    pub refs_heads_path: PathBuf,
}

impl DirectoryManager {
    pub fn new<T: Into<PathBuf>>(base_path: T) -> Self {
        let base_path: PathBuf = base_path.into();
        let dot_git_path = base_path.join(".git");

        // Initialize them
        Self {
            //...
            branches_path: dot_git_path.join("branches"),
            objects_path: dot_git_path.join("objects"),
            refs_tags_path: dot_git_path.join("refs").join("tags"),
            refs_heads_path: dot_git_path.join("refs").join("heads"),
            dot_git_path,
        }
    }

        pub fn is_dot_git_empty(&self) -> Result<bool, std::io::Error> {
        Ok(!self.dot_git_path.exists() || self.dot_git_path.read_dir()?.next().is_none())
    }

    pub fn create_directory_tree(&self) -> Result<(), std::io::Error> {
        //...
        // Create them!
        fs::create_dir_all(&self.branches_path)?;
        fs::create_dir_all(&self.objects_path)?;
        fs::create_dir_all(&self.refs_heads_path)?;
        fs::create_dir_all(&self.refs_tags_path)?;
        Ok(())
    }
}

I also added one more unit test to directory_manager module. You can see it here.

Let's add a few tests for GitRepository as well. Crate a new file tests/repository.rs and a first test:

#[test]
fn if_project_directory_is_empty_create_should_be_successful() {
    // Create an empty directory
    let project_dir = test_utils::general::generate_random_path();
    std::fs::create_dir_all(&project_dir).unwrap();

    let repo = GitRepository::create(&project_dir).unwrap();

    // Sub-directories should be created.
    let dir_manager = &repo.directory_manager;
    assert!(dir_manager.dot_git_path.exists());
    assert!(dir_manager.branches_path.exists());
    assert!(dir_manager.objects_path.exists());
    assert!(dir_manager.refs_heads_path.exists());
    assert!(dir_manager.refs_tags_path.exists());

    // .git/config should have default configs
    assert_eq!(
        fs::read_to_string(&dir_manager.config_file).unwrap(),
        GitConfig::default_str()
    );
}

The rest of the tests are self-explanatory. See them here