Thursday 20 October 2016

Gaia Integration Test Framework

As any software developer will tell you, good tests are the key to good code. It's with that in mind that Gaia aims to provide a simple and robust way to write and maintain integration tests for your Hackmud scripts. The best way to explain how to write tests with Gaia is by example, so let's dive straight in.


Getting Down to Earth with Gaia


Let's suppose we have the following extremely simple script:

function(context, args)
{
    var nickname = args.nickname;
    var message = "Hello, " + nickname;

    return message;
}

The script expects a single argument (nickname) and then produces a greeting accordingly. Not hugely ambitious, but a good place to start.

Running kode.greeting

Let's write a test for this. I'll show you the whole thing first, and what it looks like when it runs. Then we'll go through it with a fine-tooth comb.

function(context, args)
{
    return {

        test_greet_me: function(g) {
            var r = #s.kode.greeting({ nickname: "big boy" });
            g.assert_equals("Hello, big boy", r,
                            "We were not greeted appropriately");
        }

    };
}

We run the test by passing it into Gaia as a scriptor, identified by the 'test_script' key:


Testing kode.greeting with kode.gaia
The test passed! Fantastic. As you can see, Gaia clearly labels which tests it has run (test_greet_me in this case), the result of the test, a summary of all tests (even though we only had the one) and an overall outcome.

Now, as promised, let's interrogate the test script. Perhaps unusually, the very first thing the function does is a return statement. This is because when we pass the script into Gaia, Gaia will immediately execute our script, expecting to be given an object containing all of the tests to run. Therefore, the only thing our script needs to do is return an object.

Update: This method of calling Gaia only works if your test script is public. This is because Gaia needs to be able to run the script you pass into it. For running private scripts, see the alternative method in the section entitled Running Private Test Scripts below.

Update 2: Since this article was written, an additional mandatory parameter, testing, has been added. See The Persistence of Memory, below, for more details.

The object we are returning has a single key/value pair, corresponding to a single test. The key we use to identify our test has some restrictions and implications. Firstly, in order for Gaia to recognise it as a test, it must begin with test_. What comes after this is entirely up to you, but good practice says that it should be a descriptive name explaining exactly what it does.

Our key test_greet_me corresponds to a function with a single parameter, g. This parameter provides access to special Gaia functions, as we'll see shortly.

Next, the test calls the greeting script (the one we're testing), passing in a nickname as an argument. Then, we call the assert_equals method on the Gaia object, g. It takes three arguments: the expected value - "Hello, big boy" in our case, the actual value, held in the return object, r, and finally, the message that will be displayed should the test fail. The third argument is optional - if none is supplied then a generic message will be shown in the case of failure - but having a descriptive message can help to quickly diagnose problems. The assert_equals method is one of a suite of assertion functions that confirm a statement to be true, and cause the test to fail if it is not. The remaining functions will be described in detail later.


Down in the Reeds


Let's now expand our greeting script. We're going to have it store nicknames it encounters in the database and, if called with one it's seen before, offer a jovial acknowledgement.

function(context, args)
{
    var l = #s.scripts.lib();
    var nickname = args.nickname;

    // Get all the name objects from the db
    var names = #db.f({ type: "nickname" }).array();
    var nicknames = null;
    var seenNameBefore = false;

    // Filter the name objects down to any that match
    seenNameBefore = names.filter(function(name) {
        return name.nickname === nickname;
    }).length > 0;

    if (!seenNameBefore)
        // Record the new name in the db
        #db.i({ type: "nickname", nickname: nickname });

    var message = "Hello, " + nickname;

    if (seenNameBefore)
        message += ". Great to see you again!"
    
    return message;
}

New stuff is highlighted. It's not too important to go into the details as the tests only really need to to understand inputs and outputs. In a nutshell, we fetch an array of objects from the database with a type of nickname. These are all the names we've recorded previously and have a key/value pair called nickname. We filter the array to see if it contains the name that was passed to the script and, if so, we add the sentence "Great to see you again!" to our greeting. If not, we store the new nickname in the database.

So let's test this new behaviour. Testing interactions with the database is one of the things Gaia allows you to do that an external testing framework does not. Because Gaia runs in the Hackmud engine, you can test anything that you can do in-game including reading to and writing from the database, and calling other scripts, whether your own or those of other players.


function(context, args)
{
    return {

        before_each_clean_db: function(g) {
            #db.r({ nickname: "big boy" });
        },

        test_greet_me: function(g) {
            var r = #s.kode.greeting({ nickname: "big boy" });
            g.assert_equals("Hello, big boy", r,
                            "We were not greeted appropriately");
        },

        test_greet_me_again: function(g) {
            #s.kode.greeting({ nickname: "big boy" });
            // Call a second time with the same nickname
            var r = #s.kode.greeting({ nickname: "big boy" });
            g.assert_equals("Hello, big boy. Nice to see you again!", r,
                            "We were not greeted appropriately");
        }

    };
}

We've added two new functions - before_each_clean_db and test_greet_me_again. The first isn't a test but a function that will run before each test, as the name implies. Any function whose key starts before_each_ will work in this way. This function simply removes the "big boy" nickname entry from the database so that each test runs from a common starting point.

The new test builds on the first by calling kode.greeting twice with the same nickname. If the behaviour works correctly, we expect a longer greeting the second time. Let's run this and see what happens.


Test failure due to a g.assert_equals failure

Oh dear. Our new test failed. Luckily, Gaia tells us exactly why it failed. We expected a response containing "Nice" when it actually contained "Great". It's now up to us to decide whether the behaviour of greeting is wrong, or if this was simply a mistake in our test. In this case, I think it's the latter - we should be testing for "Great". If we correct the test script and run it again, both test cases should pass.


Two passing tests

That's better. We always prefer green to red.

Let's try one more thing before we wrap up the walk-through. What happens if our test causes an actual error? Let's change the first test to refer to a variable that doesn't exist for the sake of demonstration:


function(context, args)
{
    return {

        before_each_clean_db: function(g) {
            #db.r({ nickname: "big boy" });
        },

        test_greet_me: function(g) {
            var r = #s.kode.greeting({ nickname: "big boy" });
            // Deliberately cause an error
            x +=3
            g.assert_equals("Hello, big boy", r,
                            "We were not greeted appropriately");
        },

        test_greet_me_again: function(g) {
            #s.kode.greeting({ nickname: "big boy" });
            // Call a second time with the same nickname
            var r = #s.kode.greeting({ nickname: "big boy" });
            g.assert_equals("Hello, big boy. Great to see you again!", r,
                            "We were not greeted appropriately");
        }

    };
}

The error is highlighted, as is the corrected second test from last time. What happens when we run it?

Reporting a test error

The first test is marked as having caused an ERROR and Gaia helpfully displays the JavaScript error message - in this case that x is undefined. The second test was still able to run and pass, despite the error in the first.

That concludes the worked example. However, there are a few more important things to mention.


The Devil is in the Details


As well as test_ and before_each_ there are a few more function key prefixes of note.

  • before_once_ - these functions run once and once only before any tests are started - if you have ten tests, these functions will still only run once! Useful for global set-up that all tests will make use of, but not change.
  • before_each_ - these functions run before every test - useful for resetting to a common state.
  • test_ - the actual test cases that test the behaviour of a script.
  • after_each_ - like before_each_ but run after every test.
  • after_once_ - like before_once_ but run once all tests have finished.

There are also additional assertion functions available as part of the Gaia object passed into each test, g. Note that all of these functions take a message as their last argument that will be displayed should the assertion fail.

  • g.assert(expected, msg) - shorthand for assert_true - see below.
  • g.assert_true(expected, msg) - asserts that expected resolves to true.
  • g.assert_false(unexpected, msg) - asserts that unexpected resolves to false.
  • g.assert_equals(expected, actual, msg) - asserts that expected equals the value of actual (by a == comparison).
  • g.assert_not_equals(unexpected, actual, msg) - asserts that unexpected does not equal the value of actual (by a != comparison).


If you'd like to see Gaia running tests right now, you can, by asking it to test itself. Simply enter the following command:

kode.gaia { self_test: true }

You should see the results of several tests. If they don't all pass, please notify me (@kode) immediately!


Running Private Test Scripts


The method of calling Gaia on the command line described above (e.g. kode.gaia { test_script: #s.user.sometest } will only work if the test script is public. This is because Gaia needs to be able to run the script you pass to it. Luckily, a simple change is all that's required to be able to run private scripts.

Instead of having your test script return an object containing your test cases, you instead simply invoke Gaia from within your script and pass the same object into it, exactly as if you were calling Gaia from the command line. A modified test_greeting now looks like this:

function(context, args)
{
    var tests = {

        before_each_clean_db: function(g) {
            #db.r({ nickname: "big boy" });
        },

        test_greet_me: function(g) {
            var r = #s.kode.greeting({ nickname: "big boy" });
            g.assert_equals("Hello, big boy", r,
                            "We were not greeted appropriately");
        },

        test_greet_me_again: function(g) {
            #s.kode.greeting({ nickname: "big boy" });
            // Call a second time with the same nickname
            var r = #s.kode.greeting({ nickname: "big boy" });
            g.assert_equals("Hello, big boy. Great to see you again!", r,
                            "We were not greeted appropriately");
        }

    };

    return #s.kode.gaia({ test_script: tests });
}

That's the only change required to your script. You now run the tests by simply calling this script on the command line e.g. kode.test_greeting. There a couple of things to note:

  1. The object containing the test cases is passed to Gaia under the test_case key, just like when Gaia is used directly on the command line.
  2. It's important to return the result of the call to Gaia from your script so that the test results are printed to the terminal.

The Persistence of Memory


Since this article was first written, an additional mandatory parameter, testing, has been added to Gaia's signature. This parameter accepts the name of the script being tested, not the test script. It's used to store the most recent test results for a given script in the database. For the example used in this tutorial, the call to Gaia would now look like this from the command line:

kode.gaia { test_script: #s.kode.test_greeting, testing: "kode.greeting" }

And from within the test script:

#s.kode.gaia({ test_script: tests, testing: "kode.greeting" });

The most recent test results for our script can be retrieved by running:


kode.gaia { get_results: "kode.greeting" }

 This yields the following:


Retrieving persisted test results

This is intended mainly to be used by other scripts e.g. for gathering statistics.

Wrapping Up


So there you have it. I hope you can see how Gaia can be extremely useful in testing your Hackmud scripts and giving you confidence that any changes you make don't compromise their functionality. If you have any comments or suggestions, or if you find any defects in Gaia, please do let me know in the comments below, or message me in-game @kode.

Have fun out there!