Customizing Lua Unit-Testing via Busted and luassert

I like to use Busted for unit-testing in Lua. Busted uses the luassert library for providing the assertions you use within your tests, e.g. assert.is.truthy(foo). The project page has an example of how to extend the library by defining your own custom assertions. But today I am going to provide a more verbose example—two in fact.

Our Goal

Here’s what we want to accomplish: we want to implement an in_array() assertion so that we can write tests such as:


assert.is.in_array("foo", some_array)
assert.is_not.in_array("bar", some_array)

Preamble

We will need both the luassert and say libraries to implement our custom assertion. So the beginning of our implementation looks like this:


local assert = require("luassert")
local say    = require("say")

say:set_namespace("en")

Our output messages for failed assertions will be in English, hence the value we give to say:set_namespace().

Defining the Assertion

Our assertion function will receive two arguments, named state and arguments by convention. Ninety-nine percent of the time we can ignore state and will have no need for it. The arguments table will contain the values given to each invocation of our assertion function, meaning when we write assert.is.in_array("foo", some_array) we will have:

arguments[1]

"foo"

arguments[2]

some_array

So the first thing we will want to do is extract those values. And then a simple for-loop will tell us if the first argument exists in the array that is the second argument. And that all naturally leads us to this implementation:


local function in_array(state, arguments)
    local element = arguments[1]
    local array = arguments[2]

    for _,value in ipairs(array) do
        if value == element then return true end
    end

    return false
end

Note well that our function returns a boolean indicating the success or failure of the assertion, as this is a requirement that custom assertions must adhere to.

With our function in place we can now register it with the luassert library. But in doing so we need to provide appropriate "positive" and "negative" error messages for failed assertions. The "positive" message will appear when something like assert.is.in_array() fails, and the "negative" when assert.not.in_array() fails. We use the say library to register these messages with specific names, and the common convention is:

assertion.NAME_OF_FUNCTION.positive
assertion.NAME_OF_FUNCTION.negative

So we end up writing this:


say:set("assertion.in_array.positive",
        "Expected value %s to be a value in array:\n%s")

say:set("assertion.in_array.negative",
        "Expected value %s to not be a value in array:\n%s")

Both messages have two instances of the %s formatting code. That is because the messages will receive the arguments given to in_array(), i.e. the two values in its arguments parameter. So we display them in the message output to help with debugging.

Now we can take the final step, registering in_array() with the assertion library itself:


assert:register("assertion", "in_array", in_array,
                "assertion.in_array.positive",
                "assertion.in_array.negative")

The arguments are:

  1. "assertion", always first by convention.
  2. The name of the assertion as a string.
  3. The function implementing the assertion.
  4. The name of the positive error message, registered with say.
  5. The name of the negative error message.

Example Usage


it("Can overwrite an existing data set via chance.set()", function ()
    chance.set(setName, setData)
    assert.is.in_array(chance.fromSet(setName), setData)
    chance.set(setName, { "Foo" })
    assert.is.in_array(chance.fromSet(setName), { "Foo" })
    assert.is.not_in_array(chance.fromSet(setName), setData)
end)

Notice how we can express the assertion in a variety of ways, e.g. assert.is.not_in_array() versus assert.in_array() versus assert.not.in_array() and so on.

I mentioned a second example. It is is_within_range(), an assertion for testing if a given number is within a certain range. Here is the entire implementation along with examples.

If you use Busted for unit-testing and need to write your own assertions, either out of convenience or necessity, then hopefully this article will prove useful.

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