JanetRossini.github.io

Lua, LSL, Blender, Python in Second Life


Project maintained by JanetRossini

Extending the Tests Framework

Jun 1, 2025 • [designluaobjectstesting]


Lua, by default, compares tables for identity, not contents equality. We would like to do better with Tests. When we’re done with this improvement, tests will diagnose incorrect, missing, and excess values in tables.

-- test passes, tables have same keys and values
function Tests:test_tables_equal()
   result = {a=5, b="hello"}
   expected = {b="hello", a=5}
   self:assert_equals(result, expected)
end

Before my changes, it does not pass, saying:

test_tables_equal: table: 0x000000002066997c was expected to  be table: 0x0000000020669954 ()

I propose to check at the top of assert_equals whether both result and expected are tables, and if they are, to check them key by key. It will be necessary to check them both ways, since either one might have keys not in the other.

I begin with this much:

function Tests:assert_equals(result, expected, message)
   if type(result) == "table" and type(expected) == "table" then
      self:assert_tables_equal(result, expected)
      return
   end
   local message = message or ""
   local correct = result == expected
   if correct then
      tests_passing += 1
   else
      test_failing += 1
      self:diagnose(result, 'was expected to  be', expected, message)
   end
end

function Tests:assert_tables_equal(result, expected)
   for k, v in pairs(expected) do
      self:assert_equals(result[k], v)
   end
   for k, v in pairs(result) do
      self:assert_equals(v, expected[k])
   end
end

My test passes. Well done, Janet, recursion worked on the first try. Now we need to check at least three more things:

  1. result has same keys but different values than expected;
  2. result is missing a key that expected has;
  3. result has excess key that expected does not have.

For all of these, I think we’ll need to create better messages than the default.

I’ll do one case at a time, as it is pretty widely considered better to do many small tests than one big one.

Here’s the next test. I expect all these will fail as intended but the messages may be hard to understand.

-- test fails
function Tests:test_result_has_wrong_value()
   result = {a=5, b="goodbye"}
   expected = {b="hello", a=5}
   self:assert_equals(result, expected)
end

Run tests. Two lines come out, no real surprise since we iterate twice. Maybe we can do better than that. Here’s the output:

 test_result_has_wrong_value: 
     goodbye was expected to  be hello ()
 test_result_has_wrong_value: 
     goodbye was expected to  be hello ()

I’d like the message to be something like this:

result key b -> goodbye, expected hello.

But the message as presently defined is appended to the default, it does not replace it. Here’s the best I can see to do:

test_result_has_wrong_value: 
    goodbye was expected to  be hello (key: b)

That much is easy:

function Tests:assert_tables_equal(result, expected)
   for k, v in pairs(expected) do
      self:assert_equals(result[k], v, `key: {k}`)
   end
   for k, v in pairs(result) do
      self:assert_equals(v, expected[k])
   end
end

Let’s change assert_tables_equal so that we don’t get duplicate messages, instead checking for excess keys only. Maybe like this:

function Tests:assert_tables_equal(result, expected)
   for k, v in pairs(expected) do
      self:assert_equals(result[k], v, `key: {k}`)
   end
   for k, v in pairs(result) do
      if expected[k] == nil then
         self:assert_equals(v, nil, "unexpected key")
      end
   end
end

That suppressed the extra messages when keys are preesent but do not have the same values. Now we need two more tests, one for insufficient keys and one for excess.

-- test fails
function Tests:test_result_has_excess_value()
   result = {a=5, b="hello", surprise="oops"}
   expected = {b="hello", a=5}
   self:assert_equals(result, expected)
end

That one gives this message:

test_result_has_excess_value: 
    oops was expected to  be nil (unexpected key)

I think the code already handles the missing value case well enough already, diagnosing a nil in result. Let’s find out:

-- test fails
function Tests:test_result_has_missing_value()
   result = {a=5}
   expected = {b="hello", a=5}
   self:assert_equals(result, expected)
end

That fails with this:

test_result_has_missing_value: 
    nil was expected to  be hello (key: b)

I think that will do. All the cases are pretty clearly delineated with the extra messages. I am confident that nested tables will work, and I’m not going to test them just now.

I think that the generic message “foo was expected to be bar” is a bit long, and the messages of course all come out prefixed by the object name, but we’ll let those niceties ride for now.

And we’ll leave all the samples in the framework so that users can see how it all works, and remove them when they write their own tests.

I’ll clean up the comments a bit before I save.

Saved and put into the files for download.

Woot! We can test tables for equality and get some pretty nice diagnostics when they’re wrong. I am pleased!