Some quick test recipes to get you started, followed by some explanation of what’s going on behind the curtain.
I’ll show some simple test recipes using the Tests object, which you can download using the buttons at the top of the screen. Then we’ll explain the tests, and then finally, the details of setting them up.
For now, just assume that you’ve followed the instructions to define the Tests object. We’ll show you how later on.
Here are some simple test recipes:
Suppose we have a table consisting of a type of animal and the number of those animals that we have, and we want to know how many animals we have in all. We might write this test:
function Tests:test_count_animals()
local animals = { aardvarks=6, badgers=11, capybaras=9}
local count = count_animals(animals)
self:assert_equals(count, 6+11+9)
end
Every test using our Tests framework follows that basic form. It is defined as a function on Tests, and its name starts with test_
followed by whatever name we want to give this test. (Each test name must be unique and begin with test_
.)
We then arrange any values that need setting up, in this case our animals table. We then take the action that should provide the test result, in this case calling count_animals
. Then we make an assertion about the result, typically self:assert_equals
. The parameters to self:assert_equals
are three:
If all our tests pass, the framework simply prints a summary:
All Asserts: Pass 1 Fail 0. Tests:run_tests
In SL that may be prefixed with the name of your object, or with Tests:
, depending on the version of the framework you have.
Suppose there was a defect in our function under test count_animals
. We if our assertion fails, we’ll see results something like this:
Actual: 33, Expected: 26. Tests:test_count_animals
All Asserts: Pass 0 Fail 1. Tests:run_tests
Each assertion that fails will result in a message describing the failure, and the count of assertions passing and failing will be updated accordingly.
The detailed form of the message depends on the kind of assertion that failed, and may be modified in upcoming versions for clarity or brevity. Suffice to say, we’ll try to keep them clear.
If your tests have a lot of similar assertions, or you’re wondering what went wrong, you can add a message parameter to the assertion and it will be printed along with the standard message if the assertion fails. Like this:
function Tests:test_count_animals()
local animals = { aardvarks=6, badgers=11, capybaras=9}
local count = count_animals(animals)
self:assert_equals(count, 6+11+9,
'We just have 6a, 11b, 9c')
end
The resulting message:
Actual: 33, Expected: 26. [We just have 6a, 11b, 9c] Tests:test_count_animals
All Asserts: Pass 0 Fail 1. Tests:run_tests
The framework handles comparing tables for equality. Here’s a simple example:
function Tests:test_tables()
local expected = {apples=3, bananas=6, cherries=12}
local actual = {bananas=6, cherries=9, durian=1}
self:assert_equals(actual, expected)
end
In our actual
above, we’re missing the key “apples”, have the wrong value for “cherries” and have an unexpected fruit, “durian”.
Result:
Actual: table[cherries]=9, Expected: 12. Tests:test_tables
Actual: table[apples] is missing, Expected: [apples]=3. Tests:test_tables
Actual: table[durian]=1, Unexpected key [durian]. Tests:test_tables
All Asserts: Pass 0 Fail 1. Tests:run_tests
Games played on square-tiled boards where only rook-style moves are allowed often use “manhattan distance” to measure the distance between two tiles, the sum of the absolute x difference and y difference between the tiles. If we needed that function, we might have a Tile class, and test it like this:
function Tests:test_manhattan_distance()
local t1 = Tile(10, 10)
local t2 = Tile( 13, 15)
local d = t1:manhattan(t2)
self:assert_equals(d, 8)
end
function Tests:test_manhattan_distance_other_way()
local t1 = Tile(10, 10)
local t2 = Tile( 13, 15)
local d = t2:manhattan(t1)
self:assert_equals(d, 8)
end
We want to be sure that the distance returned is the same no matter if the second tile is to our left or right or above us or below us, so we write two tests, one each way.
Here again, we arrange our objects, two in this case, t1
and t2
. We act on them, asking for the distance d
. And we assert that d
is 8.
Each test is a function defined on the Tests object, with a unique name beginning with test_
.
Generally a test will look like this:
When tests are run, each assertion that fails will print a detailed message, and a summary of assertions passing and failing is produced at the end.
That’s really all there is to it. Repeat with tests and code until you’re sure the code is doing what you want it to do. When in doubt, test it. When it breaks, write more tests, including one showing it breaking, then fix the broken code.
Here’s an overview of the process:
class()
function, the “combined” framework, using the buttons at the top of the page.A bit more detail:
This should be easy enough. Decide which framework you want and press the button. Combined, if present, includes both Tests and class()
.
Generally one will want the class()
function near the top of one’s script, as it will be used often to create classes throughout your script. The Tests will refer to your own objects and functions, so probably needs to be placed near the bottom of the script.
(This advice may change, or be out of date as the framework grows and SLua evolves. Basically things need to be defined before they are used, although judicious use of local declarations at the top of one’s script can help. At this writing, standard ways of doing things are still evolving.)
The framework suggests versions of state_entry
and touch_start
that automatically run the tests. In your actual script you’ll probably want something different.
See also Packaging below.
A good way to be sure you have things in the right places is to run the tests as is. At this writing you’ll see something like Pass 7 Fail 5, but that may change as the framework evolves. You may wish to review the samples in some detail, as they are intended to be useful examples of how to write tests and how test error messages appear.
Remove the samples and write your first test. Only actual assertions are counted. I often write a trivial first test just to be sure I’ve got everything set up right, perhaps:
function Tests:test_wiring()
self:assert_equals(3, 4, "wired up correctly")
end
Just keep adding tests as you need them, run them often.
At this writing, SLua is still new—in alpha release in fact—so we do not have much advice yet on packaging. Here are preliminary thoughts:
Where script space permits, I think I would try to leave the Tests framework and my tests in my real script. That will make changes more safe, as I can easily extend and rerun my tests.
To save script space, of course, we may wish to have our production scripts include only what is absolutely needed. In that case I envision at least three reasonable possibilities:
If Linden Labs is able to give us a require
or include
capability, production configuration may be easier. We can hope.
Feedback on this article, and any article, is welcome. I’m Janet Rossini in world, or valkyriejanet at gmail dot com.
Happy testing, and safe paths!