Running Unit Tests via git-push

I use Git for source-control management on almost all of my projects. Git allows you to write hooks to perform arbitrary actions when you create commits, merge branches, fetch updates, and so on. Today I want to show you how I use the pre-push hook to run test suites automatically.

Our Example

I am going to use my Chance library for the example use-case. The project uses Busted for its test suite, and Tup for its build system. Running tup within the project directory will, among other things, execute the test suite; worth noting for this article is that tup also runs Luacheck on the Chance source code, which will catch any errors in the Lua code along with warning me about a variety of stylistic issues.

Our Goal

Here’s what we want to do:

  1. Check to see if we are pushing updates to the master branch, i.e. are we running git push origin master.

  2. If so, we want to run tup in order to run the test suite and Luacheck on the source code.

  3. If either of those fail, then we want to prevent the push.

The ultimate result is that git push origin master will fail if there is anything wrong with the code.

Our Implementation

All Git hooks go into the .git/hooks/ directory of a project, as executable scripts with specific names. In this case we need to write the .git/hooks/pre-push hook. That script must perform the actions described above.

Here is the entire hook script:

#!/bin/sh

remote="$1"
url="$2"

while read local_ref local_sha remote_ref remote_sha
do
    if [ "$local_ref" == "refs/heads/master" ]
    then
        cd "$(git rev-parse --show-toplevel)" && tup upd > /dev/null
        exit $?
    fi
    exit 0
done

The script receives two arguments: the name of the remote destination and its URL. If wanted we could use these to only run the test suite when pushing to origin, which would be the value of $remote whenever we run git push origin master. But here it’s not necessary.

The script receives more information from standard input, which will have a single line representing everything we’re pushing. By using read we can bind this information to shell variables. Since we want to run the test suite when pushing the master branch the variable we’re most interested in is $local_ref. As you can see from the script, its value will be refs/heads/master when pushing that branch. So that’s what we search for to determine whether to run the test suite.

As previously mention, running tup upd will invoke the build system, which executes the test suite and more. But we want to run it from the top-level directory of the project. We could do something like cd ../../, but a more elegant solution is to use git rev-parse --show-toplevel. This command is useful for obtaining the top-level directory of a Git project no matter where we are in the directory tree. For example, on my computer the Chance library is in the /home/eric/Projects/chance.lua directory, which is exactly the value that git rev-parse --show-toplevel returns regardless of where we run that command within the project. This leads us to this code:

cd "$(git rev-parse --show-toplevel)" && tup upd > /dev/null
exit $?

It changes to the top-level directory, runs tup upd, and exits with the status code from Tup, i.e. $?. If Tup runs successfully, meaning the test suite passed, then $? will be equal to zero; if our pre-push script exits with zero then Git will proceed with the push. But if anything goes wrong during the build process then pre-push will exit with a non-zero value, preventing git push from doing anything.

Note that if $local_ref is not equal to refs/heads/master then we immediately exit with a value of zero, allowing us to push any branch except master without requiring a successful build.

Conclusion

Hopefully the example and explanation is simple and straight-forward. In general what you want to do with pre-push is…

  1. Decide if you want to run your tests based on the remote to which you are pushing.

  2. Decide which local branches, if any, should act as explicit triggers for the tests you want to run.

  3. And then run any appropriate tests, making sure to exit with the value zero if everything is successful, and taking advantage of git rev-parse --show-toplevel to make it easier to navigate the project’s directory structure to run any such tests.

Here’s another example from a different project, which is more simple than the example in this article since it runs tests regardless of the remote and local branch being pushed.

Advertisements

Add Your Thoughts

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s