How to automatically run unit tests from a git push

Tags:

Typically when working on code that has tests, you would want to make sure your tests pass when you share your code with other people. It’s generally a good idea to run the tests once in a while too, but why do it manually when you can automate?

In this post I’ll show a simple shell script you can use to plug into git, and automatically run your testsuite and optionally reject a push with failing tests.

Git hooks

Git provides several ways to hook into the various things that happen. For example, there’s a pre-commit hook, a pre-receive hook, a post-receive hook and a few other hooks.

For this, we’re going to use the pre-receive hook. This can be used to execute a script before the changes from a push are written into the repository.

Setup

First you’ll need to set up the tool(s) you wish to run from the script. In my case, I’ve installed PHPUnit on my server and verified it runs from the command-line.

For the git pre-receive hook to run before a push, it must be placed into the server which holds the git repository. I probably should point out that this should be a bare repository.

In my case, the path to my git repository is /home/jani/repos/example, so the hook file would be /home/jani/repos/example/hooks/pre-receive – in other words, it’s placed into the hooks dir in the repo.

You will also need to decide where you want the script to put its temporary files – in my case, I’m just going to use /home/jani/tmp/example

The git pre-receive hook

#!/bin/bash
while read oldrev newrev refname
do
    # Only run this script for the master branch. You can remove this 
    # if block if you wish to run it for others as well.
    if [[ $refname = "refs/heads/master" ]] ; then
        # Anything echo'd will show up in the console for the person 
        # who's doing a push
        echo "Preparing to run phpunit for $newrev ... "
 
        # Since the repo is bare, we need to put the actual files someplace, 
        # so we use the temp dir we chose earlier
        git archive $newrev | tar -x -C /home/jani/tmp/example
 
        echo "Running phpunit for $newrev ... "
 
        # This part is the actual code which is used to run our tests
        # In my case, the phpunit testsuite resides in the tests directory, so go there
        cd /home/jani/tmp/example/tests       
 
        # And execute the testsuite, while ignoring any output
        phpunit > /dev/null
 
        # $? is a shell variable which stores the return code from what we just ran
        rc=$?
        if [[ $rc != 0 ]] ; then
            # A non-zero return code means an error occurred, so tell the user and exit
            echo "phpunit failed on rev $newrev - push deniend. Run tests locally and confirm they pass before pushing"
            exit $rc
        fi
    fi
done
 
# Everything went OK so we can exit with a zero
exit 0

This should hopefully be quite self-explanatory with the comments. Basically git sends the old revision, new revision and the ref’s name in standard input to the script, so we first read those into variables. After, we simply use git archive to get a tar of the revision, extract it to our temp dir, and run whatever tools we want. You might’ve noticed that it’s actually a loop – that’s because a git push may contain more than one commit, and as such we’d like to run the tests for all the commits to be sure which one broke things (if any).

Returning a non-zero exit code from the shell script will cause the push to be denied.

In my case, I decided to only run this script on the master branch. This is because occasionally you might want to push some work-in-progress code in a dev branch or such. I also wanted to ignore any phpunit output, because there might be a lot of it, and instead just provide a message to the user telling them to run tests. You can modify this behavior by removing the > /dev/null part to allow phpunit output, or the if-block to allow other branches.

In closing

This is basically a poor man’s continuous integration solution, or sort of anyway. Depending on how many tests you have, you might want to actually run an actual continuous-integration server (such as Jenkins), which would allow you to run a a bigger testsuite separate of a push. This is because if you have a script running which takes a long time, it will cause the git push to be delayed until its completion.