-
Notifications
You must be signed in to change notification settings - Fork 17
Unit testing
For my own peace of mind, I have developed a unit testing regime. Unit testing is testing each unit of your software to ensure it does what you expect it to do. To make this as easy as possible, the testing process is usually automated so you just click a button, and the system runs all your tests, reporting at the end which, if any, failed. This is very common is software development (in fact, I would hope it is compulsory!), so is something you might want to consider too, especially if you have a lot of custom functions, templates or commands.
For the development of QuestJS, I created a game specifically for this purpose (it is in this repository as "game-eg"). It has pretty much every feature in the game, so is not very coherent as a game, but does allow he to check everything works. This means that if I discover a better way to do something or add a new feature, after I have implemented it I can run all the tests and make sure everything works as it should, and I have not inadvertently broken some other part of the system.
For you own game, you would add a single file to your game folder, tests.js, and put all the tests in there.
Unit testing can be frustrating! I find about two thirds of the bugs it finds are bugs in my tests rather than my actual code, and it can feel like you are wasting your time. However, it does pick up real bugs, and when it does it is usually easier to locate the source. In the long run it is definitely worthwhile for complicated games.
To turn testing on, you need to set settings.tests
to true
in settings.js. You also need settings.playMode
to be set to "dev" (it probably is already).
settings.playMode = "dev"
settings.tests = true
When you come to release your game, ensure settings.playMode
is set to "play", and then you can skip the file from your game package and "lib/test-lib.js" (though it makes little difference if you include the files).
Your tests should go in a file called "game/tests.js", inside a function called "tests" in the "test" object. Here is a simple example:
"use strict"
test.tests = function() {
test.start("Simple object commands")
test.assertCmd("i", "You are carrying a knife.")
test.assertCmd("get knife", "You have it.")
test.title("Going up")
test.assertCmd("u", ["You head up.", "A large room, with a big bed and a wardrobe.", /^You can see/, "You can go in or down.",])
}
The test.title
function just says what this section of the tests is. If you get an error, Quest will report which test number in which section was at fault, so the title is just a way to find the failing test faster. I suggest no more than ten tests in a section, but it is up to you.
The test.start
function does that, but it also resets your game back to its initial state. There are a couple of issues to be aware. The tests use the standard Quest save/load system. When the tests start, a save game is created (the name is set in test.saveFilename
and defaults to "unit-test-save-file"). This means that the test.start
function only resets to whatever the game state was when the tests were run - so you should always reload the page, and then run the tests immediately.
The reset is only as good as the save/load system. If you change attributes of the "settings" object, attributes of exits, etc. these are not (by default) saved, so will not get reset. If you cannot fathom why something is not working, it may be worthwhile commenting out all the earlier sections and see if the bug is still there. If not, it may be an issue with save/load.
The test.assertCmd
function runs the command, and compares the output with what was expected. The first parameter is the command, just as the player might type it. The second parameter is the expected response, and can be either a string for an exact match or a regular expression. It can also be an array of strings and/or regular expressions for commands that output more than one paragraph, as in the third one above.
You should test failures too! It is important that you test that the player cannot do things she should not be able to, and that the game gives a sensible response. In the second test above, we are testing the player cannot get the knife she is already carrying.
If there is some text in italics or bold or some other styling, behind the scenes there will be some HTML codes embedded in the text. If you want to check the text is styled, then you can include the codes in the text to test against, but often this is just a pain in the neck.
You can set test.ignoreHTML
to true
and all he HTML will be stripped out. In fact, you can turn test.ignoreHTML
on and of as required through your unit tests.
Web pages do not always display text exactly - for example two spaces together will be displayed as one. This means you can occasionally have a test fail, and yet the two strings look identical. One solution is to add a third parameter to test.assertCmd
, set to true
.
test.assertCmd("u", ["You head up.", "A large room, with a big bed and a wardrobe.", /^You can see/, "You can go in or down.",], true);
This will compare each dissimilar string character by character, and tell you the ASCII code and position where they are different (in the console window, so F12 to see). If the first difference is due to character number 32, that is an extraneous space character. Count along the string to that position to find it.
As an alternative, it can be helpful to look at the raw HTML. Open up the developer tools (F12) and go to the Elements tab, then dig down to find the offending strings. Copy-and-pasting the outer HTML from there into a text processor may help too.
To handle a menu, you must first set test.menuResponseNumber
to the number of the response.
test.menuResponseNumber = 1;
test.assertCmd("speak to lara", /^You tell Lara/);
It is probably more important to test functions and similarcode. In this case, use the test.assertEqual
function, which takes two parameters, the first being the expected value, the second being the result of your code.
This example tests a custom text processor directive that returns "sir" if "callmemaam" is false
and "ma'ma" if true
.
test.tests = function() {
test.title("Text processor");
test.assertEqual("Sir! Yes, sir", processText("{Sir}! Yes, {sir}"))
game.player.callmemaam = true
test.assertEqual("Ma'am! Yes, ma'am", processText("{Sir}! Yes, {sir}"))
}
When building a complicated system, unit testing is an excellent way to test each building block and give you the confidence that they are working.
If you are testing a function that prints to the screen, you need a different approach, because we need to capture that output, rather than have it go to screen, and compare that to the expected. We can use the test.assertOut
function - which test.assertCmd
also uses behind the scenes.
It is a little more complicated as it needs to be given a function. Here is a simple example. The first parameter is an array of strings; the expected output as with a command. The second is a Boolean indicating if extra output is wanted (see later). The third is the function.
test.assertOut(["Output from functionToTest"], false, functionToTest)
If you want to pass parameters to your function, you will need to put it inside another function. Similarly, if the function belongs to an object (and uses this
). These two examples illustrate.
test.assertOut(["Kyle says hello."], false, function() {
functionToTest(w.Kyle)
})
test.assertOut(["Kyle says hello."], false, function() {
w.Kyle.functionToTest()
})
Alternatively, you can use test.function
to test a function. This takes just the function as a parameter, and returns an array of strings - whatever would be printed to screen. This example does exactly the same as the first example above. If your function will produce a dozen lines of output and you only care about the third one, this may be easier.
const result = test.function(function() {
functionToTest(w.Kyle)
})
test.assertEqual("Kyle says hello.", result[0])
If you turn on test.fullOutputData
both these functions will get arrays of dictionaries, rather than arrays of strings, allowing you to further check output. The advantage of test.function
becomes more apparent as we can check each bit individually.
test.fullOutputData = true
let res
res = test.function(function() { msg("#Kyle is {select:Kyle:colours:colour}.") })
test.assertEqual("default-h default-h4", res[0].cssClass)
test.assertEqual("h4", res[0].tag)
test.assertEqual("Kyle is red.", res[0].text)
test.fullOutputData = false
Remember to turn off test.fullOutputData
when done.
You can also just test the game state. Here is an example that tests locking and hiding exits. The test.assertEqual
compares an expected value against the actual value, while test.assertMatch
compares a regular expression against a string. Note that it is often helpful to set up the situation first.
test.title("Lock and hide");
const room = w.far_away;
test.assertEqual(true, room.hasExit("north"));
test.assertEqual(true, room.hasExit("north", true));
test.assertEqual(false, room.setExitLock("northeast", true));
test.assertEqual(true, room.setExitLock("north", true));
test.assertEqual(false, room.hasExit("north", true));
test.assertEqual(true, room.hasExit("north"));
room.templatePreSave();
const landh = room.getSaveString();
test.assertMatch(/customSaveExitnorth\:\"locked\/\"/, landh);
It is often useful to move the player object to a different location so test things there. Using the normal world.setRoom
will cause messages to be printed to the screen and could trigger various scripts. A better way is this:
test.movePlayer('lounge')
Often you will only be interested in the first one or two paragraphs that get output (you do not really want to test the list of objects and the list of exits every time you move to another room), so we have the padArray
function. This takes an array of strings you do want to test, and adds a set number of generic arrays that match anything.
test.assertCmd("e", test.padArray(["The kitchen is very messy."], 2));
You need to think carefully when this is appropriate to use; it is kind of cheating the tests.
By default, the game will reset back to its starting point after the tests complete. Often, it is convenient to keep it at that final point, so you can check what the game looks like there. Set test.resetOnCompletion
to false
to turn off resetting (I suggest doing this at the top of your tests.js file, after "use strict").
test.resetOnCompletion = false
If you have a vast number of tests, you might want to be reassured they are actually getting done. Put this at the top of your tests.js file, after "use strict", and the section titles will appear in the console as they are started.
test.printTitles = true
Once you have your testing in place, you might wonder how much of your code you are actually testing. Having two dozen tests for one command and no tests for the other seventeen is not great! This is called code coverage in the unit testing world, and is an important metric; Chrome therefore has a built in coverage tool. Open the developer tools with F12, and you should see a coverage tab third along in the lower section. When you run your tests, you should see each file listed, together with a bar indicating how big it is and how much has been used.
Files with a lot of red in the bar need more testing!
That said, there will be several files that belong to the Quest 6 libraries and other libraries. In the image above, the file at the top that is nearly all red is JQuery; it is not mine, it is from a well-established library, so I can safely ignore it.
If you double-click on a file, it will appear in the upper pane, and lines that have not been used with be flagged in red at the left of the line, so you can tell exactly what needs to be tested.
Some things are easier to test than others. Functions are easy, so should always be tested. Novel effects on the screen are pretty much impossible to unit test.
What do you do if your game has randomness? For an RPG, combat may depend on the roll of virtual dice, which can make testing tricky. How can you check the outcome if it cannot be predicted?
The way to do this is to "prime" the random.int
function, so it is predictable. The random.prime
function takes a number or array of numbers to a buffer. If there is a number in that buffer, random.int
will return that number instead of a random one.
In the tests, you first load up the array with some values, then check what is produced when they are used:
test.title("Random");
random.prime([4, 19])
test.assertEqual(4, random.int())
test.assertEqual(19, random.int())
Note that all the functions in random
ultimately use the random.int
function, so that is the only one you need to re-write. For example:
random.buffer.push(3)
random.buffer.push(8)
test.assertEqual(11, random.dice('2d6'))
This also illustrates that random.int
will return any number given, whether it makes sense or not. In this example you roll a 3 and an 8 on a six-sided dice!
You can readily test events just by calling the eventScript
function on an object. If you want five turns to pass, call it five turns. You can then compare values before and after.
test.assertEqual("In flight", probe.status)
test.assertEqual(1, probe.launchCounter)
for (let i = 0; i < 5; i++) probe.eventScript()
test.assertEqual("Exploring", probe.status)
test.assertEqual(6, probe.launchCounter)
If your events produce output (some text appears on screen), you can stop it appearing by setting test.testing
to true
(turn it back to false
after). You can also capture it in test.output
, allowing you to test what it is.
test.testing = true;
test.testOutput = [];
test.assertEqual("In flight", probe.status)
test.assertEqual(1, probe.launchCounter)
for (let i = 0; i < 5; i++) probe.eventScript()
test.assertEqual("Exploring", probe.status)
test.assertEqual(6, probe.launchCounter)
test.testing = false;
test.assertEqual("'Bio-probe I has successfully landed on the planet.'", test.testOutput[0])
A quick way to create tests is to subvert the transcript system. Paste this code into code.js:
io.scriptShow = function(opts) {
let html = '';
html += '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"/><title>Quest 6 Unit Tests for '
html += settings.title
html += '</title></head><body><p>"use strict"</p><p>test.tests = function() {</p><p>test.assertCmd("script on", ['
for (let el of this.transcriptText) {
switch (el.cssClass) {
case 'input': html += '])</p><p> test.assertCmd("' + el.text + '", ['; break;
default : html += '"' + el.text.replace(/\"/g, '\\"') + '",'; break;
}
}
html += '])</p><p>}</p></body></html>'
const tab = window.open('about:blank', '_blank')
tab.document.write(html)
tab.document.close()
}
Then play your game. Start by doing SCRIPT ON to turn transcripts on, then go do everything (perhaps from a walk-through). When you are done, do SCRIPT SHOW, and a new tab will appear with the contents of the tests.js file. The first and last can probably be deleted, as that will be the SCRIPT commands. Any menu choices will need attention too.
When you are done, comment out the above code so transcripts work normally again.
Tutorial
- First steps
- Rooms and Exits
- Items
- Templates
- Items and rooms again
- More items
- Locks
- Commands
- Complex mechanisms
- Uploading
QuestJS Basics
- General
- Settings
- Attributes for items
- Attributes for rooms
- Attributes for exits
- Naming Items and Rooms
- Restrictions, Messages and Reactions
- Creating objects on the fly
- String Functions
- Random Functions
- Array/List Functions
- The
respond
function - Other Functions
The Text Processor
Commands
- Introduction
- Basic commands (from the tutorial)
- Complex commands
- Example of creating a command (implementing SHOOT GUN AT HENRY)
- More on commands
- Shortcut for commands
- Modifying existing commands
- Custom parser types
- Note on command results
- Meta-Commands
- Neutral language (including alternatives to "you")
- The parser
- Command matching
- Vari-verbs (for verbs that are almost synonyms)
Templates for Items
- Introduction
- Takeable
- Openable
- Container and surface
- Locks and keys
- Wearable
- Furniture
- Button and Switch
- Readable
- Edible
- Vessel (handling liquids)
- Components
- Countable
- Consultable
- Rope
- Construction
- Backscene (walls, etc.)
- Merchandise (including how to create a shop)
- Shiftable (can be pushed from one room to another)
See also:
- Custom templates (and alternatives)
Handing NPCs
- Introduction
- Attributes
- Allowing the player to give commands
- Conversations
- Simple TALK TO
- SAY
- ASK and TELL
- Dynamic conversations with TALK TO
- TALK and DISCUSS
- Following an agenda
- Reactions
- Giving
- Followers
- Visibility
- Changing the player point-of-view
The User Experience (UI)
The main screen
- Basics
- Printing Text Functions
- Special Text Effects
- Output effects (including pausing)
- Hyperlinks
- User Input
The Side Panes
Multi-media (sounds, images, maps, etc.)
- Images
- Sounds
- Youtube Video (Contribution by KV)
- Adding a map
- Node-based maps
- Image-based maps
- Hex maps
- Adding a playing board
- Roulette!... in a grid
Dialogue boxes
- Character Creation
- Other example dialogs [See also "User Input"]
Other Elements
- Toolbar (status bar across the top)
- Custom UI Elements
Role-playing Games
- Introduction
- Getting started
- Items
- Characters (and Monsters!)
- Spawning Monsters and Items)
- Systema Naturae
- Who, When and How NPCs Attack
- Attributes for characters
- Attacking and guarding
- Communicating monsters
- Skills and Spells
- Limiting Magic
- Effects
- The Attack Object
- [Extra utility functions](https://github.com/ThePix/QuestJS/wiki/RPG-Library-%E2%80%90-Extra Functions)
- Randomly Generated Dungeon
- Quests for Quest
- User Interface
Web Basics
- HTML (the basic elements of a web page)
- CSS (how to style web pages)
- SVG (scalable vector graphics)
- Colours
- JavaScript
- Regular Expressions
How-to
Time
- Events (and Turnscripts)
- Date and Time (including custom calendars)
- Timed Events (i.e., real time, not game time)
Items
- Phone a Friend
- Using the USE verb
- Display Verbs
- Change Listeners
- Ensembles (grouping items)
- How to spit
Locations
- Large, open areas
- Region,s with sky, walls, etc.
- Dynamic Room Descriptions
- Transit system (lifts/elevators, buses, trains, simple vehicles)
- Rooms split into multiple locations
- Create rooms on the fly
- Handling weather
Exits
- Alternative Directions (eg, port and starboard)
- Destinations, Not Directions
Meta
- Customise Help
- Provide hints
- Include Achievements
- Add comments to your code
-
End The Game (
io.finish
)
Meta: About The Whole Game
- Translate from Quest 5
- Authoring Several Games at Once
- Chaining Several Games Together
- Competition Entry
- Walk-throughs
- Unit testing
- Debugging (trouble-shooting)
Releasing Your Game
Reference
- The Language File
- List of settings
- Scope
- The Output Queue
- Security
- Implementation notes (initialisation order, data structures)
- Files
- Code guidelines
- Save/load
- UNDO
- The editor
- The Cloak of Darkness
- Versions
- Quest 6 or QuestJS
- The other Folders
- Choose your own adventure