Writing End-to-End Tests

We will continue to use Ginkgo and Gomega as our testing framework, but we will invoke multi-git as a command-line program and will not test at the Go-program level.

There are several interesting parts to test such as the environment, the happy path, and error handling. We will tackle all of them.

Testing the environment

A big part of interacting with multi-git is telling it where the root directory of all the git repos it should operate on is as well as the names of these repos (the sub-directories). As you recall, this is done by setting two environment variables: MG_ROOT and MG_REPOS. The unit tests for the repo_manager package tested the input to the NewRepoManager() function, but it didn’t check the reading and parsing of environment variables themselves. Let’s write some tests at this level. As you recall, RunMultiGit sets the MG_ROOT and MG_REPOS environment variables for us. The two tests are pretty simple. The first test passes a non-existent directory as the root directory. The second test passes an empty repository list. Both tests verify that RunMultiGit() returns an error and also checks the output to ensure the correct error message was returned.

    Context("Tests for empty/undefined environment failure cases", func() {
        It("Should fail with invalid base dir", func() {
            output, err := RunMultiGit("status", false, "/no-such-dir", repoList)
            Ω(err).ShouldNot(BeNil())
            suffix := "base dir: '/no-such-dir/' doesn't exist\n"
            Ω(output).Should(HaveSuffix(suffix))
        })

        It("Should fail with empty repo list", func() {
            output, err := RunMultiGit("status", false, baseDir, repoList)
            Ω(err).ShouldNot(BeNil())
            Ω(output).Should(ContainSubstring("repo list can't be empty"))
        })
    })

Let’s test the happy path where the command actually succeeds.

Testing the happy path

We tested the happy path with the repo_manager unit tests, so we know that when we pass the correct input repo_manager will do the job. However, it’s important to test parsing the command-line arguments and the environment variables when they are valid.

We will write three test cases:

  1. git init on uninitialized directories
  2. git status on initialized repositories
  3. git branch on initialized repositories

Here’s the first test, which creates directories dir-1 and dir-2 using CreateDir(), but it DOES NOT initialize them because we want multi-git to perform the initialization. Then, we call RunMultiGit() with the “init” command and check the output to ensure we see two instances of the expected message: “Initialized empty Git repository”.

    Context("Tests for success cases", func() {
        It("Should do git init successfully", func() {
            err = CreateDir(baseDir, "dir-1", false)
            Ω(err).Should(BeNil())
            err = CreateDir(baseDir, "dir-2", false)
            Ω(err).Should(BeNil())
            repoList = "dir-1,dir-2"

            output, err := RunMultiGit("init", false, baseDir, repoList)
            Ω(err).Should(BeNil())
            count := strings.Count(output, "Initialized empty Git repository")
            Ω(count).Should(Equal(2))
        })

The second test looks very similar, except that when creating the directories we tell CreateDir() to initialize them as well. Then, we call RunMultiGit with the “status” command, again making sure we see two counts of the expected message: “nothing to commit”.

        It("Should do git status successfully for git directories", func() {
            err = CreateDir(baseDir, "dir-1", true)
            Ω(err).Should(BeNil())
            err = CreateDir(baseDir, "dir-2", true)
            Ω(err).Should(BeNil())
            repoList = "dir-1,dir-2"

            output, err := RunMultiGit("status", false, baseDir, repoList)
            Ω(err).Should(BeNil())
            count := strings.Count(output, "nothing to commit")
            Ω(count).Should(Equal(2))
        })

The last test doesn’t add anything new, but it performs an action on initialized repositories to create a branch.

        It("Should create branches successfully", func() {
            err = CreateDir(baseDir, "dir-1", true)
            Ω(err).Should(BeNil())
            err = CreateDir(baseDir, "dir-2", true)
            Ω(err).Should(BeNil())
            repoList = "dir-1,dir-2"

            output, err := RunMultiGit("checkout -b test-branch", false, baseDir, repoList)
            Ω(err).Should(BeNil())

            count := strings.Count(output, "Switched to a new branch 'test-branch'")
            Ω(count).Should(Equal(2))
        })

We tested what happens when the environment is incorrect and we tested the happy path. However, we still need to test what happens when the command fails for other reasons.

Testing error handling

One of the most common reasons git fails is because you ask it to perform actions on a directory that is not a git directory. It can happen if you forget to use git init on your target directory or, even more commonly, the target directory is in the wrong location. The following test creates two uninitialized directories and checks that the correct error message is returned when trying to run git status:

    Context("Tests for non-git directories", func() {
        It("Should fail git status", func() {
            err = CreateDir(baseDir, "dir-1", false)
            Ω(err).Should(BeNil())
            err = CreateDir(baseDir, "dir-2", false)
            Ω(err).Should(BeNil())
            repoList = "dir-1,dir-2"

            output, err := RunMultiGit("status", false, baseDir, repoList)
            Ω(err).Should(BeNil())
            Ω(output).Should(ContainSubstring("fatal: not a git repository"))
        })
    })

In addition to flat out failures, multi-git has the option to continue running on other repositories in its list even if the processing failed on some of the repositories. These tests create directories where the first directory (“dir-1”) is not initialized, but the second directory (“dir-2”) is initialized. When running multi-git on these the result depends on the value of the ignoreErros flag. When it’s true multi-git should return an error for “dir-1” and continue running after the failure to successfully run the command against “dir-2”:

    Context("Tests for ignoreErrors flag", func() {
        Context("First directory is invalid", func() {
            When("ignoreErrors is true", func() {
                It("git status should succeed for the second directory", func() {
                    err = CreateDir(baseDir, "dir-1", false)
                    Ω(err).Should(BeNil())
                    err = CreateDir(baseDir, "dir-2", true)
                    Ω(err).Should(BeNil())
                    repoList = "dir-1,dir-2"

                    output, err := RunMultiGit("status", true, baseDir, repoList)
                    Ω(err).Should(BeNil())
                    expected := "[dir-1]: git status\nfatal: not a git repository"
                    Ω(output).Should(ContainSubstring(expected))
                    expected = "[dir-2]: git status\nOn branch master"
                    Ω(output).Should(ContainSubstring())
                })
            })

However, when ignoreErrors is false multi-git should bail out after the failure on “dir-1”

            When("ignoreErrors is false", func() {
                It("Should fail on first directory and bail out", func() {
                    err = CreateDir(baseDir, "dir-1", false)
                    Ω(err).Should(BeNil())
                    err = CreateDir(baseDir, "dir-2", true)
                    Ω(err).Should(BeNil())
                    repoList = "dir-1,dir-2"

                    output, err := RunMultiGit("status", false, baseDir, repoList)
                    Ω(err).Should(BeNil())
                    expected := "[dir-1]: git status\nfatal: not a git repository"
                    Ω(output).Should(ContainSubstring(expected))
                    Ω(output).ShouldNot(ContainSubstring("[dir-2]"))
                })
            })

Get hands-on with 1400+ tech skills courses.