JavaScript's Zillionth Testing Harness
JavaScript’s Zillionth Testing Harness
Jazillionth is a lightweight, non-intrusive, easy-to-use testing harness for automatically testing your JavaScript;
As expected, Jazillionth doesn’t promise the world and beyond, but its lightweight design is sufficient for most scenarios.
All use cases are explained in the chapters below, and examples are shown in the chapter Examples.
Compared to other simple testing harnesses, the only ‘complicated’ thing Jazillionth does is load the pages to test in an iframe. Other testing harnesses require you to add the testing code onto the actual pages themselves. This means you need to have mechanisms in place to strip this testing code out of your pages again once they are ready to be put into production. By loading the pages into an iframe, your pages can remain totally oblivious to the test suite you wrap around it. This also makes it much more likely your pages are truly tested stand-alone and do not unknowingly rely on the testing code itself to be present (even in production).
This does however have drawbacks.
Due to browser security measures (Same Origin policy), both the test suite page and the page under test must be served from the same domain. One can get around this by using postMessage
, but that is out of scope for Jazillionth.
Local files (opening files with the file://
protocol) are not testable either. However, see the chapter Testing without a dedicated web server for instructions on how to overcome this one.
Last (but certainly not least), both the test suite page as well as the pages under test running in the iframe have their own scripts. But the test suite page’s script should not run before the page under test’s script has run. Jazillionth seems to get this right, but I have so far not been able to get absolute guarantees that the way Jazillionth manages this is a sure-fire way to ascertain this behavior. If Jazillionth’s tests run before your page under test’s scripts have run, please let me know!
If the above restrictions are a deal breaker in your situation, you can still use Jazillionth in inline testing mode. In that case you only need to add three link tags to your page’s head section to link in Jazillionth and your JavaScript test script. This way an iframe is not necessary anymore which relaxes all security restraints mentioned above. You should then be able to run your tests anywhere and anyhow you like, while still only minimally altering the page to test.
Jazillionth is licensed under the MIT license. See the LICENSE file for details.
This document refers to two types of pages: the test suite page, which you use to run tests, and the page under test, which is the subject of the tests.
In this document all Jazillionth object instances are named jazil
for consistency, but you’re free to name it whatever you want. This name reappears as the name of the central Jazillionth object as well as the name of the argument passed into all callback functions.
You can run Jazillionth in three ways;
Option 1 allows you to keep your testing code totally separate from the page under test. This way the page under test can remain free of testing code, so that you can be confident that what you test can be put into production as-is. In some circumstances this is unfortunately not possible, but then you can always use option 2.
Jazillionth doesn’t require much to get going. The bare minimum that is needed on either the test suite page or the page under test is the following, in the given order:
By default, Jazillionth requires nothing else. In the simplest use case it will:
If you want to add extra content to the test suite page you are free to do so. You can add your own iframe to load the page under test into and tell Jazillionth to use that. You can also tell Jazillionth where to place the test log, directing it to an existing HTML element instead. And the used colors are tweakable too. And you can add pre and post test handlers to really tweak the test flow, pausing the tests where needed. See the chapter Tweaking and advanced functionality for more details.
Before you can register your tests, you have to start up Jazillionth. You do this by creating a single Jazillionth variable. You can make this a global variable so that the rest of your scripts can access it automatically, though every time Jazillionth calls back on you it will pass the actual Jazillionth object in as an argument too.
When creating the Jazillionth object, you can also pass in an ‘options’ argument to further tweak Jazillionth. See the chapter Tweaking and advanced functionality for more details on this.
To start, simply add:
let jazil = new Jazillionth()
and Jazillionth is up-and-running, waiting for pages and tests to be registered.
When you test your JavaScript variables, classes, functions, etc., you want to access them in the simplest way possible. Your scripts can already access everything they need as-is if you test the current page inline. But Jazillionth loads external pages in an iframe, and then all those JavaScript objects are tucked away there too. Jazillionth helps with this by letting you specify a list of the names of objects you want to access, and then makes them accessible for you like they were living inside your own script.
To get a bit technical: when making these objects accessible to your script, Jazillionth takes a reference to these global objects and puts that reference in your script’s global scope. For classes and functions e.g., this can be done as-is. For objects that do not get re-assigned anymore once created, this also works. But dynamic variables that store literal values (like numbers and strings) cannot be referenced like this. Jazillionth can only copy their value instead, which means you get a snapshot of the value they had when Jazillionth started up, and live updates are not visible to your script.
To solve this, Jazillionth employs a workaround for these types of objects: you can also provide a list of the names of objects to track. When doing so, these objects will be made accessible to your scripts via a like-named accessor function instead of a like-named variable. Do note though that you then need to treat that variable as a function to get at its value; so instead of using let copy = myValue
in your test script, you’d have to use let copy = myValue()
. A small price to pay.
Jazillionth allows you to test multiple pages simultaneously, each with its own sets of tests. First you add a page to test, then you add test set(s) to that page to test; these test sets hold the individual test functions themselves. The object hierarchy thus becomes:
To add a page to test, call AddPageToTest(name, url, accessObjectNames, trackObjectNames)
for external pages, or AddPageToTest(name)
for the current page;
name
: stringurl
: string (optional)accessObjectNames
: array of string (optional)trackObjectNames
: array of string (optional)accessObjectNames
, but for tracked objects. Jazillionth will make accessor functions for these objects for you.AddPageToTest
returns the created TestPage object to you. You must use this to later register test sets for it. Note that Jazillionth ignores the accessObjectNames
and trackObjectNames
when testing the current page inline, since your scripts can access these objects as-is anyway, and accessor functions for trackObjectNames
would even give name conflicts with the objects they track.
You can also access its properties wherever you encounter it in your callback functions and test functions. These are:
name
: stringurl
: stringaccessObjectNames
: array of string, or undefined
undefined
.trackObjectNames
: array of string, or undefined
undefined
.testSets
: array of TestSet objectsExample:
let mainPage = jazil.AddPageToTest('External page', '/path/to/main-page-to-test.html')
or
let mainPage = jazil.AddPageToTest('Current page')
With this TestPage variable mainPage
in hand, you are ready to add one or more named test sets to it. These test sets form their own sections in the test log. This allows you to, for example, define a different test set per tested module in that page to make the test log line up nicely with your internal code structure for an easier overview.
Test sets themselves are a combination of a name and a simple dictionary holding the test functions. In this dictionary, each key is the name of the individual test function, and each value is the function performing that test. You register test sets with Jazillionth by calling AddTestSet(testPage, name, testFunctions)
;
testPage
: TestPage objectAddPageToTest
, representing the page to test to add the test set to.name
: stringtestFunctions
: dictionary of test functionsThe resulting TestSet object will get the following properties:
name
: stringtests
: dictionary of test functionsThe test functions themselves should have the following signature (synchronous and asynchronous test functions are supported);
function(jazil, testName, testSet, testPage) {
}
async function(jazil, testName, testSet, testPage) {
}
The function arguments are:
jazil
: Jazillionth objecttestName
: stringtestSet
: TestSet objecttestPage
: TestPage objectYou do not need to return anything from a test function. Also note that if you have no use for the latter arguments, you do not need to add them to your test function signature.
Example:
let tests = {
'Test X': function(jazil) {
// test X comes here
},
'Test Y': function(jazil, testName, testSet, testPage) {
// test Y comes here
// testName should be 'Test Y'
// testSet should be the TestSet object we're part of
// testPage should be the TestPage object we got from AddPageToTest (stored in mainPage)
}
}
jazil.AddTestSet(mainPage, 'Main tests', tests)
Note that the passed in Jazillionth object and the one you created yourself are really the same thing.
Inside your test functions you perform the actual test(s) using the appropriate assertion functions on the given jazil
object. The results of your tests are implicitly determined by the result of these assertion functions. You therefore do not need to return anything from your function; if all tests pass (so no assertion function got triggered), the test function itself is marked as passed too. Do note that as soon as an assertion function fails, the rest of the code in your test function after the call to that assertion function will not execute anymore.
The following assertion functions can be used:
jazil.Fail(message)
jazil.Assert(condition, message)
jazil.ShouldBe(value, expected, message)
===
operator. The message is augmented to show the actual and expected values. Note that NaN is also explicitly considered equal to NaN (a departure from the Javascript default).jazil.ShouldNotBe(value, expected, message)
jazil.ShouldBe
.jazil.ShouldBeLoose(value, expected, message)
jazil.ShouldBe
but uses the loose non-strict ==
equality comparison instead.jazil.ShouldNotBeLoose(value, expected, message)
jazil.ShouldBeLoose
.jazil.ShouldBeBetween(value, expectedLower, expectedHigher, message)
>=
and <=
operators. The message is augmented to show the actual and expected values.jazil.ShouldNotBeBetween(value, expectedLower, expectedHigher, message)
jazil.ShouldBeBetween
.jazil.ShouldThrow(CodeToRun, message)
CodeToRun()
). You can thus supply any lambda or function object.jazil.ShouldNotThrow(CodeToRun, message)
jazil.ShouldThrow
. The message is augmented with the thrown exception converted to text.jazil.SkipTest()
jazil.ShouldBe
or such, the test will still be counted and failed.Note that with all these assertion functions the message is optional. Jazillionth will always show where the fail happened (file and line number), so if a test only has one single check the message is probably redundant (the test name and the details added by, for example, ShouldBe
should say enough). But if a test function calls more than one assertion function, the message can be used to easily pinpoint which assertion failed.
Note also that there is no opposite to jazil.Fail
; if a test goes well, you do not have to report anything.
When your test function implicitly or explicitly throws an exception itself, Jazillionth will catch that too and mark the test as ‘failed’. If you know that the code your unit tests could throw an exception, you thus do not have to wrap that call in a try/catch block to prevent the tests breaking. However, depending on the type of exception used, you will or will not get a proper call stack trace in that failed test’s logs. When your code uses the standard Error
exception type (or any other type derived from it), a call stack trace is added. All other exception types (e.g., throw
ing raw strings and such) will most likely not get a call stack trace. Do note though that while support for getting the call stack trace is common in all major browsers, it is not standardized (yet) and might thus not always work.
If your test function needs to test asynchronous functions, you must await
on the result of these asynchronous function calls before exiting the test function itself. This also implies that the test function itself needs to be marked async
. If you don’t, Jazillionth will happily assume the test passed and it will continue with the next test.
Since external pages under test are loaded in a separate iframe, your testing scripts cannot easily access its HTML content nor its JavaScript functions and global variables, etc. But Jazillionth will help out with this.
When adding an external page to test you can tell Jazillionth to automatically make certain JavaScript objects available for direct use in your scripts. Do this by specifying the accessObjectNames
argument to AddPageToTest
. You should pass an array holding the names of all the JavaScript objects you want to access on that page. Once that page has loaded, Jazillionth will try to access each one of these objects and make an alias to them under the test suite page’s window, so that they will also be globally available. After that these objects are accessible just as if they were declared in your own script.
The only restriction here is that global variables which are set to literal values (numbers, string etc.) get copied. This means you cannot modify the originals in the page under test, nor do the values your script sees change while your tests run. A similar thing applies when a global variable holds an object but is assigned a different object while the tests run. For these objects you can prepend their name with an ‘!’ in the accessObjectNames
list, like !myVariable
. Jazillionth will then add an accessor function under that variable’s name, instead of a copy of the value. When you want to access the value you then only need to treat the variable as a function, like myVariable()
.
When you test the current page inline, no JavaScript objects are made available, since your script should be able to access them by itself anyway. You can still pass a list of names in the accessObjectNames
argument when you pass undefined
as the URL argument, but these names will just be ignored. This way you can more easily switch between external and inline testing.
Jazillionth also makes two other objects accessible: the current page under test’s window
and document
objects. You can find these under jazil.testWindow
and jazil.testDocument
respectively. You can use these to access, for example, the page under test’s body content, the rest of the page under test’s global JavaScript var
variables and functions, as well as the page’s location
, history
and localstorage
objects.
When testing the current page inline these properties are set to the page’s own window
and window.document
objects, so that your test scripts can be kept agnostic of what type of page is being tested.
After you created the test suite page, just open it in your browser. Once the test suite page is fully loaded, Jazillionth will load all pages under test one by one. For each page under test, once it is fully loaded, all tests in all registered test sets for that page will be executed automatically. When all pages have been tested, Jazillionth will show the outcome of each test in the test log. It will also adjust the test log’s background color to the overall test outcome, whereby if only one test fails, the overall test outcome is a failure too.
And just press Refresh whenever you want to run the tests again!
Simple pages with simple content can be tested statically without problem. The same goes for individual test sets which only perform tests on statically scoped test conditions. For these tests, it is OK to just run all tests on all pages back-to-back without interruption; this is what Jazillionth does by default.
However, when your test sets depend on user interaction, you can alter the test flow by using custom event handlers and/or starting/pausing/continuing the tests on your own terms. These can all be controlled via initialization options. The following mechanisms are in place for this:
startAutomatically
false
, Jazillionth will not start automatically when the test suite page has loaded. If you want the tests to start at another time (for example after pressing a button on the test suite page or after waiting for some AJAX request to finish first), you can set this to false
and explicitly call StartTests()
on the Jazillionth object when you are ready to start the tests.OnBeforePageTests
OnBeforePageTests(jazil, testPage)
. jazil
is the fully set-up Jazillionth object doing the tests, and testPage
is the TestPage object returned by the original call to AddPageToTest
. You can return true
to stop all tests in their tracks at this point; returning in any other way will make the tests continue.OnAfterPageTests
OnAfterPageTests(jazil, testPage, testedOK)
. jazil
is the fully set-up Jazillionth object doing the tests, testPage
is the TestPage object returned by the original call to AddPageToTest
, and testedOK
is a boolean indicating if all tests ran OK for this page.OnBeforeSetTests
OnBeforeSetTests(jazil, testPage, testSet)
. jazil
is the fully set-up Jazillionth object doing the tests, testPage
is the TestPage object returned by the original call to AddPageToTest
, and testSet
is the TestSet object returned by the original call to AddTestSet
. You can return true
to stop all tests in their tracks at this point; returning in any other way will make the tests continue.OnAfterPageTests
OnAfterSetTests(jazil, testPage, testSet, testedOK)
. jazil
is the fully set-up Jazillionth object doing the tests, testPage
is the TestPage object returned by the original call to AddPageToTest
, testSet
is the TestSet object returned by the original call to AddTestSet
, and testedOK
is a boolean indicating if all tests ran OK for this test set.If you let Jazillionth pause its testing, then you can continue the tests at a later time by calling ContinueTests
on the Jazillionth object. For a more finely controlled code execution flow you can pass an optional boolean argument to indicate you want a delayed continuation. With delayed continuation the tests will only resume running after your current script is done. (This is arranged via a setTimeout with a delay of zero.)
Do note that StartTests
and ContinueTests
are asynchronous functions, and they will return before the tests have finished. So if you want your script to wait for the tests to be done before the rest of your script runs, you have to await
on them.
The presentation of the test results and the exact working of Jazillionth can be changed. To do so, pass in an extra 2nd ‘options’ key/value dictionary object when creating the Jazillionth object. You do not have to specify all individual options; if one is missing, its default value is used instead.
The complete list of all Jazillionth options is:
resultElementSpec
: css element selector (default: undefined
)iframeElementSpec
: css element selector (default: undefined
)passColor
: css color code (default: '#008000'
)failColor
: css color code (default: '#800000'
)textColor
: css color code (default: '#ffffff'
)showPassedTests
: bool (default: false
)showPassedTests
option sets the initial state of this button.showResultWhenDone
: bool (default: true
)false
.ignoreCallStackLinesWith
: array of string (default: ''
)jazillionth.js
is always added for you to this list. Do note that case sensitive string comparison is used!startAutomatically
: bool (default: true
)OnBeforePageTests
: custom event handler (default: undefined)OnAfterPageTests
: custom event handler (default: undefined)OnBeforeSetTests
: custom event handler (default: undefined)OnAfterPageTests
: custom event handler (default: undefined)If you do not have a simple web server to run your tests with, all is not lost! There are multiple solutions out there which make running a local web server very easy. For instance, if you have Python installed, then you already have everything you need. Just open a terminal / command line, go to the folder holding your files to test (your desired ‘web root’), and enter:
python -m SimpleHTTPServer
python -m http.server
If your test suite page is in a subfolder named ‘testing’ and named ‘testSuite.html’, you can then open your browser and go to:
http://localhost:8000/testing/testSuite.html
If you have Python 3 installed, then you can also use testExamples.py
from the root of this repository. This is a python script which automatically starts a web server serving the examples, and which opens all examples in your default browser. It also instructs your browser to disable caching for all content served, so that you can more easily test changes to your files.
Removed the dependency on jQuery.
Jazillionth can now scroll the test result into view after all tests have ran.
Jazillionth now raises a descriptive exception when you specify an unknown object in either a ‘accessObjectNames’ or ‘trackObjectNames’ list, instead of quitting due to an error.
Jazillionth now supports async test functions. This also implies that the core testing functions are now completely asynchronous too, which gives UI responsiveness.
BREAKING CHANGE: If your code starts and/or continues tests manually by explicitly calling StartTests and ContinueTests, and if there is more code following these calls, you might want to await when calling these functions too. The old behavior was that calls to StartTests and ContinueTests would only return once the tests finished, but now these functions return immediately so that the code below these calls starts running before the tests have completed.
You can now call jazil.SkipTest() from within your unit tests to skip that test. The test will then not be counted towards the total number of tests, nor will it end up in the test log.
When comparing with NaN, Javascript treats NaN === NaN as false and NaN !== NaN as true; exactly the opposite of how other values compare. This makes it impossible to use ShouldBe and all related tests when your testing values can sometimes both equal NaN. ShouldBe etc. now explicitly makes NaN compare equal to NaN.
It’s perfectly fine to throw your own exceptions from within your unit tests or any code your unit test calls. Jazillionth will just fail that test automatically. You thus do not need to meticulously use try/catch blocks in your unit tests to prevent exceptions from leaking out.
Documentation on best practices and how this all works was however missing. The general documentation has been updated, especially on how to retain a call stack trace in the logs. Example #6 has also been augmented to show how Jazillionth reacts to unit tests throwing their own exceptions.
And since Jazillionth has been in use for quite a while now while not showing any issues, I’ve decided to drop the ‘Beta’ label.
BREAKING CHANGE FIRST: the optional startup option IgnoreCallStackLinesWith has been case-corrected into ignoreCallStackLinesWith, identical to other same-functioning options.
And Jazillionth gained a workaround for testing live updates to global variables containing literal numbers and strings.
Plus an internal re-write to use more new-fangled ECMAScript 6.0 functionality; it’s already 2023 after all.
Jazillionth can now also test pages inline, and removed the restriction of what type of objects can be accessed from the test script.
As per the title; added enough extras to warrant a new release by now.
BEWARE: this is a breaking change!
jazil.ShouldBe from now on does a strict === compare instead of a regular == compare. This is done to make all ShouldBe assertions comparing to zero-like values behave safely out-of-the-box. Previously you’d have to remember to use ShouldBeStrict in that case, which is error prone.
jazil.ShouldNotBe has been adapted likewise, and the ShouldBeStrict and ShouldNotBeStrict methods have been replaced with ShouldBeLoose and ShouldNotBeLoose to keep the regular comparison functionality available.
Added multi-page testing and test flow pausing/continuation via user supplied event handlers.
Initial beta release; might still be a bit rough around the edges.
All examples below can also be found in the examples
folder in this repository. Since it’s far easier if all examples just use the main jazillionth.js
file, the file hierarchy is a bit upside down; all test suite pages reach high up to the root of the repository to include the jazillionth.js
file. Therefore, when running these examples as-is, ensure your web root is pointing to the repository root.
We want to test a simple HTML page which uses a Summer
class to sum two numbers on-the-spot. The Summer
class is located in its own file summer.js
, and the main page code is located in the file main.js
.
File main.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="scripts/summer.js"></script>
<script src="scripts/main.js"></script>
</head>
<body>
1 + 4 = <span id="result">?</span>
</body>
</html>
File scripts/summer.js
:
class Summer {
#finalized = false
#sum = 0
Add(value) {
if (!this.#finalized)
this.#sum += value
}
get result() {
this.#finalized = true
return this.#sum
}
get canAdd() {
return !this.#finalized
}
}
File scripts/main.js
:
document.addEventListener('DOMContentLoaded', () => {
let summer = new Summer
summer.Add(1)
summer.Add(4)
document.getElementById('result').textContent = summer.result
})
File testing/tests.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="../../../jazillionth.js"></script>
<script src="tests.js"></script>
</head>
<body></body>
</html>
File testing/tests.js
:
let jazil = new Jazillionth()
let mainPage = jazil.AddPageToTest('main', '../main.html', ['Summer'])
jazil.AddTestSet(mainPage, 'Summer tests', {
'Summer should know 1 + 1': function(jazil) {
let summer = new Summer
summer.Add(1)
summer.Add(1)
jazil.ShouldBe(summer.result, 2)
},
'Summer should finalize': function(jazil) {
let summer = new Summer
summer.Add(1)
jazil.ShouldBe(summer.result, 1, 'sole number added not returned')
jazil.Assert(!summer.canAdd, 'summer is not finalized')
summer.Add(1)
jazil.ShouldBe(summer.result, 1, 'summer keeps adding after finalization')
}
})
jazil.AddTestSet(mainPage, 'Main page tests', {
'The main page should list the correct answer': function(jazil) {
let shownResult = parseInt(jazil.testDocument.getElementById('result').textContent)
jazil.ShouldBe(shownResult, 5)
}
})
We want to specify the test suite page’s layout and color scheme ourselves. For that we make the following alterations to the base example:
File testing/tests.html
: replace <body></body>
with:
<body>
<h1>Page-under-test</h1>
<iframe id="testFrame"></iframe>
<div id="testResult">
<h1>Test results</h1>
</div>
<h1>Thanks for testing!</h1>
</body>
File testing/tests.js
: replace the jazil initialization with:
let options = {
'resultElementSpec': '#testResult',
'iframeElementSpec': '#testFrame',
'passColor': '#000080' // blue is a better green
}
let jazil = new Jazillionth(options)
The main page is now going to use a new sum, as well as show a complex multiplication!
we have an extra class Multiplier
to test, so together with the Summer
tests and the main page tests this is all getting over our head fast. We’d better divide up our testing code into a configuration part and three testing set parts (one for each class plus one for the main page). Each of these four parts gets its own JavaScript file. We again adjust the base example, like so:
File scripts/multiplier.js
: a new file; give it the following content:
class Multiplier {
#finalized = false
#product = 1
Add(value) {
if (!this.#finalized)
this.#product *= value
}
get result() {
this.#finalized = true
return this.#product
}
get canAdd() {
return !this.#finalized
}
}
File scripts/main.js
: replace its content with:
document.addEventListener('DOMContentLoaded', () => {
let summer = new Summer
summer.Add(1)
summer.Add(2)
summer.Add(4)
document.getElementById('sumResult').textContent = summer.result
let multiplier = new Multiplier
multiplier.Add(2)
multiplier.Add(3)
multiplier.Add(5)
document.getElementById('multiplyResult').textContent = multiplier.result
})
File main.html
: add the following script include:
<script src="scripts/multiplier.js"></script>
File main.html
: replace <body></body>
with:
<body>
1 + 2 + 4 = <span id="sumResult">?</span><br>
2 * 3 * 5 = <span id="multiplyResult">?</span>
</body>
File testing/tests.js
: we’re going to divide this up over several files, so it can be deleted.
File testing/configure.js
: this one only gets the configuration part:
let jazil = new Jazillionth()
let mainPage = jazil.AddPageToTest('main', '../main.html', ['Summer', 'Multiplier'])
File testing/summer.js
: this gets the test set for Summer
:
jazil.AddTestSet(mainPage, 'Summer tests', {
'Summer should know 1 + 1': function(jazil) {
let summer = new Summer
summer.Add(1)
summer.Add(1)
jazil.ShouldBe(summer.result, 2)
},
'Summer should finalize': function(jazil) {
let summer = new Summer
summer.Add(1)
jazil.ShouldBe(summer.result, 1, 'sole number added not returned')
jazil.Assert(!summer.canAdd, 'summer is not finalized')
summer.Add(1)
jazil.ShouldBe(summer.result, 1, 'summer keeps adding after finalization')
}
})
File testing/multiplier.js
: this gets the test set for Multiplier
:
jazil.AddTestSet(mainPage, 'Multiplier tests', {
'Multiplier should know 2 * 3': function(jazil) {
let multiplier = new Multiplier
multiplier.Add(2)
multiplier.Add(3)
jazil.ShouldBe(multiplier.result, 6)
},
'Multiplier should finalize': function(jazil) {
let multiplier = new Multiplier
multiplier.Add(2)
jazil.ShouldBe(multiplier.result, 2, 'sole number added not returned')
jazil.Assert(!multiplier.canAdd, 'multiplier is not finalized')
multiplier.Add(10)
jazil.ShouldBe(multiplier.result, 2, 'multiplier keeps adding after finalization')
}
})
File testing/main.js
: this gets the test set for the main page:
jazil.AddTestSet(mainPage, 'Main page tests', {
'The main page should list the correct sum': function(jazil) {
let shownResult = parseInt(jazil.testDocument.getElementById('sumResult').textContent)
jazil.ShouldBe(shownResult, 7)
},
'The main page should list the correct multiplication': function(jazil) {
let shownResult = parseInt(jazil.testDocument.getElementById('multiplyResult').textContent)
jazil.ShouldBe(shownResult, 30)
}
})
File testing/tests.html
: this should now link to the following scripts. Note that we need to list configure.js
first, since that one creates the jazil
object that our other scripts need.
<script src="configure.js"></script>
<script src="summer.js"></script>
<script src="multiplier.js"></script>
<script src="main.js"></script>
Summer gets a little boost – it now tracks whether it has been used, and throws a nasty exception when using it the wrong way! So, let’s check for that. And we didn’t really properly test Summer
before, so let’s make up for that too. And what do you know: we made a mistake in our test, so one of ‘em fails now. We alter the base example in the following way:
File scripts/summer.js
: replace it with the following code:
let g_summerUsed = false
class Summer {
#finalized = false
#sum = 0
Add(value) {
if (this.#finalized)
throw 'Sorry, we\'re closed for today.'
else
this.#sum += value
}
get result() {
this.#finalized = true
g_summerUsed = true
return this.#sum
}
get canAdd() {
return !this.#finalized
}
}
File testing/tests.js
: replace it with the following code:
let jazil = new Jazillionth()
let mainPage = jazil.AddPageToTest('main', '../main.html', ['Summer', 'g_summerUsed'])
jazil.AddTestSet(mainPage, 'module Summer', {
'Summer should have been used by the test page by now': function(jazil) {
jazil.Assert(g_summerUsed, 'g_summerUsed is not set yet')
},
'Positive numbers': function(jazil) {
let summer = new Summer
summer.Add(1)
summer.Add(2)
jazil.ShouldBe(summer.result, 3, 'basic sum')
summer = new Summer
summer.Add(12)
summer.Add(9)
jazil.ShouldBe(summer.result, 21, 'sum with carry')
summer = new Summer
summer.Add(1234567)
summer.Add(3456789)
jazil.ShouldBe(summer.result, 4691356, 'big numbers')
},
'Negative numbers': function(jazil) {
let summer = new Summer
summer.Add(-1)
summer.Add(2)
jazil.ShouldBe(summer.result, 1, 'neg + bigger pos = pos')
summer = new Summer
summer.Add(3)
summer.Add(-7)
jazil.ShouldBe(summer.result, -4, 'pos + bigger neg = neg')
summer = new Summer
summer.Add(-11)
summer.Add(-31)
jazil.ShouldBe(summer.result, -42, 'neg + neg = neg')
},
'0 is a no-op': function(jazil) {
let summer = new Summer
summer.Add(231)
summer.Add(0)
jazil.ShouldBe(summer.result, 231, 'number + 0')
summer = new Summer
summer.Add(0)
summer.Add(-82376)
jazil.ShouldBe(summer.result, -82376, '0 + number')
summer = new Summer
summer.Add(0)
summer.Add(0)
jazil.ShouldBe(summer.result, 0, '0 + 0')
},
'Summer is properly initialized out of the gate': function(jazil) {
let summer = new Summer
jazil.ShouldBe(summer.result, 0, 'not properly 0')
summer = new Summer
jazil.ShouldNotBe(summer.result, undefined, 'not properly initialized')
},
'Order is irrelevant': function(jazil) {
let summer1 = new Summer
summer1.Add(2)
summer1.Add(1)
let summer2 = new Summer
summer2.Add(1)
summer2.Add(2)
jazil.ShouldBe(summer1.result, summer2.result, 'simple numbers')
summer1 = new Summer
summer1.Add(-8)
summer1.Add(25)
summer2 = new Summer
summer2.Add(25)
summer2.Add(-8)
jazil.ShouldBe(summer1.result, summer2.result, 'add in a negative')
},
'Calling Result should close the summer': function(jazil) {
let summer = new Summer
jazil.Assert(summer.canAdd, 'new Summer not addable')
summer.Add(3)
jazil.Assert(summer.canAdd, 'used Summer not addable')
summer.Add(4)
jazil.ShouldBe(summer.result, 7, 'sum not correct')
if (summer.canAdd)
jazil.Fail('closed Summer still addable')
},
'Calling Result should inhibit further addition': function(jazil) {
let summer = new Summer
jazil.ShouldNotThrow(
function() {
summer.Add(3)
},
'adding to unclosed summer'
)
summer.result
jazil.ShouldThrow(
function() {
summer.Add(4)
},
'adding to closed summer'
)
},
'Estimating hard to predict sums': function(jazil) {
let summer = new Summer
summer.Add(3)
summer.Add(5)
jazil.ShouldBeBetween(summer.result, 2, 10, 'small sum not correct')
summer = new Summer
summer.Add(30)
summer.Add(50)
jazil.ShouldBeBetween(summer.result, 20, 100, 'big sum not correct')
},
'All basic sums': function(jazil) {
// just to be sure
for (let number1 = -10; number1 <= 10; ++number1) {
for (let number2 = -10; number2 <= 10; ++number2) {
let summer = new Summer
summer.Add(number1)
summer.Add(number2)
jazil.ShouldBe(summer.result, number1 + number2, 'sum not correct')
}
}
}
})
jazil.AddTestSet(mainPage, 'Main page tests', {
'The main page should list the correct answer': function(jazil) {
let shownResult = parseInt(jazil.testDocument.getElementById('result').textContent)
jazil.ShouldBe(shownResult, 5)
}
})
The numbers to sum on the main page are now not static anymore but are maintained via localStorage
. We need to test if that goes well as well. The basic example can be changed this way:
File main.html
: change <body></body>
into:
<body>
<p>Press Refresh to calculate the next sum.</p>
<p>1 + <span id="value2">?</span> = <span id="result">?</span></p>
</body>
File scripts/main.js
: update it to:
document.addEventListener('DOMContentLoaded', () => {
let value1 = 1
let value2 = parseInt(localStorage.getItem('value2'))
if (isNaN(value2))
value2 = 0
else
++value2
let summer = new Summer
summer.Add(value1)
summer.Add(value2)
let result = summer.result
document.getElementById('value2').textContent = value2
localStorage.setItem('value2', value2)
document.getElementById('result').textContent = result
localStorage.setItem('result', result)
})
File testing\tests.js
: update the main page test set to:
jazil.AddTestSet(mainPage, 'Main page tests', {
'The main page should calculate the correct answer': function(jazil) {
let storedValue2 = parseInt(jazil.testWindow.localStorage.getItem('value2'))
let storedResult = parseInt(jazil.testWindow.localStorage.getItem('result'))
let shownResult = parseInt(jazil.testDocument.getElementById('result').textContent)
jazil.Assert(!isNaN(storedResult), 'stored result is not numeric')
jazil.Assert(!isNaN(storedValue2), 'stored value2 is not numeric')
jazil.Assert(!isNaN(shownResult), 'shown result is not numeric')
jazil.ShouldBe(shownResult, 1 + storedValue2, 'shown result is not correct')
jazil.ShouldBe(shownResult, storedResult, 'shown result is off from stored result')
},
})
The main page now lets the user enter the sum himself. So automatically running the tests when the page under test is ready is not an option anymore, because at that point the user hasn’t had a chance to interact with the page yet. We therefore start the tests ourselves with a button press. Plus, the main page now stores the result in localStorage
too; we need to test if that goes well as well. The basic example can be changed this way:
File main.html
: change <body></body>
into:
<body>
<form>
Enter the values:<br>
<input id="value1" type="number" value="1"> + <input id="value2" type="number" value="2"> = <input id="result" type="number"><br>
<input id="calculate" type="button" value="Calculate">
</form>
</body>
File scripts/main.js
: update it to:
function Calculate() {
let value1 = parseInt(document.getElementById('value1').value)
let value2 = parseInt(document.getElementById('value2').value)
let summer = new Summer
summer.Add(value1)
summer.Add(value2)
let result = summer.result
document.getElementById('result').value = result
localStorage.setItem('result', result)
}
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('calculate').addEventListener('click', Calculate)
})
File testing/tests.html
: update the body content to:
<body>
<form><input id="startTests" type="button" value="Start tests"></form>
</body>
File testing\tests.js
: update it to:
let pauseMainTests = true
function OnBeforePageTests(jazil, testPage) {
alert('Running test page "' + testPage.name + '"')
// Hijack the 'Calculate' button in the main page to eventually
// resume testing.
jazil.testDocument.getElementById('calculate').addEventListener('click', () => {
pauseMainTests = false
// Continue delayed, so that we're sure the page's own code
// ran for the calculate button.
jazil.ContinueTests(true)
alert('Main tests underway...')
})
}
function OnAfterPageTests(jazil, testPage, testedOK) {
alert('Done running test page "' + testPage.name + '"; ' + (testedOK ? 'tests passed' : 'tests failed'))
}
function OnBeforeSetTests(jazil, testPage, testSet) {
let testSetDescription = 'test set "' + testPage.name + '/' + testSet.name + '"'
let pauseJazillionth = testSet === mainSet && pauseMainTests
if (pauseJazillionth)
alert('Pausing ' + testSetDescription + '\n\nPress Calculate on the main page to resume tests.')
else
alert('Running ' + testSetDescription)
return pauseJazillionth
}
function OnAfterSetTests(jazil, testPage, testSet, testedOK) {
alert('Done running test set "' + testPage.name + '/' + testSet.name + '"; ' + (testedOK ? 'tests passed' : 'tests failed'))
}
let options = {
'startAutomatically': false,
'OnBeforePageTests': OnBeforePageTests,
'OnAfterPageTests': OnAfterPageTests,
'OnBeforeSetTests': OnBeforeSetTests,
'OnAfterSetTests': OnAfterSetTests
}
let jazil = new Jazillionth(options)
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('startTests').addEventListener('click', () => {
pauseMainTests = true
jazil.StartTests()
})
})
let mainPage = jazil.AddPageToTest('main', '../main.html', ['Summer'])
let summerSet = jazil.AddTestSet(mainPage, 'Summer tests', {
'Summer should know 1 + 1': function(jazil) {
let summer = new Summer
summer.Add(1)
summer.Add(1)
jazil.ShouldBe(summer.result, 2)
},
'Summer should finalize': function(jazil) {
let summer = new Summer
summer.Add(1)
jazil.ShouldBe(summer.result, 1, 'sole number added not returned')
jazil.Assert(!summer.canAdd, 'summer is not finalized')
summer.Add(1)
jazil.ShouldBe(summer.result, 1, 'summer keeps adding after finalization')
}
})
function GetMainPageState(jazil) {
let value1 = parseInt(jazil.testDocument.getElementById('value1').value)
let value2 = parseInt(jazil.testDocument.getElementById('value2').value)
return {
'value1': value1,
'value2': value2,
'shownResult': parseInt(jazil.testDocument.getElementById('result').value),
'storedResult': parseInt(jazil.testWindow.localStorage.getItem('result')),
'correctResult': value1 + value2
}
}
let mainSet = jazil.AddTestSet(mainPage, 'Main page tests', {
'The main page should show the correct answer': function(jazil) {
let state = GetMainPageState(jazil)
jazil.ShouldBe(state.shownResult, state.correctResult, 'shown sum is not correct')
},
'The main page should store the correct answer': function(jazil) {
let state = GetMainPageState(jazil)
jazil.ShouldBe(state.storedResult, state.correctResult, 'stored sum is not correct')
}
})
We’ve abstracted some repetitive testing stuff away by creating a testing library. We can call this library when we need to test if Summer sums two values correctly, and it will call us back when the results don’t pan out.
This does mean that we will also see this library helper function on all error call stacks when Summer goes rogue. We know however that the library is well-tested and OK, so we are not interested in seeing it reported. We can ignore this library in all error call stacks by setting the Jazillionth option ignoreCallStackLinesWith
to testLibrary.js
.
We’ll also show that this Jazillionth setting works by mucking up Summer. When you uncomment this option, you’ll see the library appears in the call stacks again.
Finally we’ll see what happens when your testing code (or libraries used by your testing code) uses exceptions themselves. Error
exceptions (or derivatives thereof) provide a nice call stack, while all other exceptions will not.
File scripts/summer.js
: Ensure we break Summer by letting it start from 1 instead of 0:
class Summer {
#finalized = false
#sum = 1
File testing/tests.html
: add our library by using the following script includes:
<script src="../../../jazillionth.js"></script>
<script src="testLibrary.js"></script>
<script src="tests.js"></script>
File testing/testLibrary
: create this file with the following content:
function TestSummer(value1, value2, OnErrorHandler) {
let correctResult = value1 + value2
let summer = new Summer
summer.Add(value1)
summer.Add(value2)
let summerResult = summer.result
if (summerResult !== correctResult)
OnErrorHandler(summerResult, correctResult)
}
File Testing/tests.js
: replace it with the following:
let options = {
'ignoreCallStackLinesWith': ['testLibrary.js']
}
let jazil = new Jazillionth(options)
let mainPage = jazil.AddPageToTest('main', '../main.html', ['Summer'])
class CustomException extends Error {
constructor(message) {
super(message)
}
}
function ThisOneThrows(message) {
throw Error(message)
}
jazil.AddTestSet(mainPage, 'Library files not in call stack', {
'Failing a test to force a call stack': function(jazil) {
TestSummer(
1, 2, (summerResult, correctResult) => {
jazil.ShouldBe(summerResult, correctResult, 'testLibrary.js should not show in the call stack')
}
)
},
'Throwing a standard exception ourselves with a call stack': function(jazil) {
throw Error('Throwing \'Error\' shows a call stack trace')
},
'Throwing a custom exception ourselves with a call stack': function(jazil) {
throw CustomException('Throwing \'CustomException\' shows a call stack trace')
},
'Throwing an exception ourselves without a call stack': function(jazil) {
throw 'Throwing a string doesn\'t show a call stack trace'
},
'Called code can also throw exceptions': function(jazil) {
ThisOneThrows('Called code is also present on the call stack trace')
}
})
We want to test the main page itself inline. This way we can test it right from disk as well without starting a server. For that we make the following alterations to the base example:
File main.html
: add extra script tags to include Jazillionth and the test scripts directly. Replace the script includes with:
<script src="scripts/summer.js"></script>
<script src="scripts/main.js"></script>
<script src="../../jazillionth.js"></script>
<script src="testing/tests.js"></script>
File testing/tests.html
: remove this file; it’s not needed anymore.
File testing/tests.js
: replace the page adding section with:
let jazil = new Jazillionth()
let mainPage = jazil.AddPageToTest('main')
We want to let Summer keep a grand total of all the values ever summed. We store this in a global variable. However, changes to global numbers can only be tracked if we explicitly ask for it by using the trackObjectNames
list when adding a page to test. To test this (and to also test that untracked variables are not tracked) we make the following alterations to the base example:
File scripts/summer.js
: replace its content with:
let g_trackedTotalSummed = 0
let g_untrackedTotalSummed = 0
class Summer {
#finalized = false
#sum = 0
Add(value) {
if (!this.#finalized) {
this.#sum += value
g_trackedTotalSummed += value
g_untrackedTotalSummed += value
}
}
get result() {
this.#finalized = true
return this.#sum
}
get canAdd() {
return !this.#finalized
}
}
File testing/tests.js
: replace the file’s content with:
let jazil = new Jazillionth()
let mainPage = jazil.AddPageToTest('main', '../main.html', ['Summer','g_untrackedTotalSummed'], ['g_trackedTotalSummed'])
jazil.AddTestSet(mainPage, 'Summer tests', {
'Summer should know 1 + 1': function(jazil) {
let summer = new Summer
summer.Add(1)
summer.Add(1)
jazil.ShouldBe(summer.result, 2)
},
'Tracked total sum should update correctly': function(jazil) {
let summer = new Summer
let oldTotalSummed = g_trackedTotalSummed()
summer.Add(3)
summer.Add(5)
jazil.ShouldBe(g_trackedTotalSummed(), oldTotalSummed + 3 + 5, 'tracked total sum is not updated correctly')
},
'Untracked total sum should not update correctly': function(jazil) {
let summer = new Summer
let oldTotalSummed = g_untrackedTotalSummed
summer.Add(4)
summer.Add(2)
jazil.ShouldNotBe(g_untrackedTotalSummed, oldTotalSummed + 4 + 2, 'untracked total sum is updated correctly')
}
})
jazil.AddTestSet(mainPage, 'Main page tests', {
'The main page should list the correct answer': function(jazil) {
let shownResult = parseInt(jazil.testDocument.getElementById('result').textContent)
jazil.ShouldBe(shownResult, 5)
}
})
We’re going to create two very related test sets, each containing a few tests. We’re going to add 1 to a number, add a number to 1, subtract 1 from a number, and subtract a number from 1. We’re going to do this for two even and two odd numbers. This should result in 16 tests in total.
However, we’re quite sure we were taught in school that it isn’t allowed to subtract with an odd number… We’re therefore going to properly skip those particular tests. These will then also not end up in the test log; in the end only 12 tests will be ran and counted.
For that we make the following alterations to the base example:
File testing/tests.js
: replace the entire content with:
let options = {
'showPassedTests': true // to show which tests ran
}
let jazil = new Jazillionth(options)
let mainPage = jazil.AddPageToTest('main', '../main.html', ['Summer'])
function IsEven(x) {
return (x % 2) === 0
}
function AddSpecificTestSet(x) {
jazil.AddTestSet(mainPage, `Summer tests - sum for x=${x}`, {
'Summer should know x+1': function(jazil) {
let summer = new Summer
summer.Add(x)
summer.Add(1)
jazil.ShouldBe(summer.result, x+1)
},
'Summer should know 1+x': function(jazil) {
let summer = new Summer
summer.Add(1)
summer.Add(x)
jazil.ShouldBe(summer.result, 1+x)
},
'Summer should know x-1 if x is even': function(jazil) {
if (!IsEven(x))
jazil.SkipTest()
// This is now safe to do; the test has already been skipped
// when x is odd.
jazil.Assert(IsEven(x))
let summer = new Summer
summer.Add(x)
summer.Add(-1)
jazil.ShouldBe(summer.result, x-1)
},
'Summer should know 1-x if x is even': function(jazil) {
if (!IsEven(x))
jazil.SkipTest()
// This is now safe to do; the test has already been skipped
// when x is odd.
jazil.Assert(IsEven(x))
let summer = new Summer
summer.Add(1)
summer.Add(-x)
jazil.ShouldBe(summer.result, 1-x)
}
})
}
AddSpecificTestSet(2)
AddSpecificTestSet(3)
AddSpecificTestSet(4)
AddSpecificTestSet(5)
We’re going to test a mix of synchronous and asynchronous functions. For that we’re going to add an asynchronous AddAsync
version to Summer
. To make it extra clear that everything is running asynchronously, we’re going to make AddAsync
also wait a bit before returning the answer, so that we can control how long our tests take. The browser should remain responsive in the meantime, so you should see the test results come in one by one (except for the one still synchronous test which should be instant).
The main page’s script is also going to use this asynchronous version. To make matters even more interesting, the main page is going to take a fair bit longer to compute than all our tests combined, so that our main page test will need to wait for the result to come in.
For that we make the following alterations to the base example:
File scripting/summer.js
: add an async version of the Add
method after the existing Add
method:
async AddAsync(value, millisecToDelay) {
await new Promise(function(resolve) {
setTimeout(resolve, millisecToDelay)
})
if (!this.#finalized)
this.#sum += value
}
File scripts/main.js
: make it use the async version in a way that takes 6 seconds, so replace the entire content with:
document.addEventListener('DOMContentLoaded', async () => {
// Wait 6 seconds in total.
let summer = new Summer
await summer.AddAsync(1, 1500)
await summer.AddAsync(2, 1500)
await summer.AddAsync(3, 1500)
await summer.AddAsync(4, 1500)
document.getElementById('result').textContent = summer.result
})
File testing/tests.js
: replace the entire content with:
let options = {
'showPassedTests': true // to see the tests coming in one by one
}
let jazil = new Jazillionth(options)
let mainPage = jazil.AddPageToTest('main', '../main.html', ['Summer'])
// This test set will take 2 seconds.
jazil.AddTestSet(mainPage, 'Summer tests', {
// This test will take 1 second.
'Summer test asynchronous #1, done at t=1 second': async function(jazil) {
let summer = new Summer
await summer.AddAsync(1, 500)
await summer.AddAsync(1, 500)
jazil.ShouldBe(summer.result, 2)
},
// This test will be instant.
'Summer test synchronous, done at t=1 second': function(jazil) {
let summer = new Summer
summer.Add(2)
summer.Add(2)
jazil.ShouldBe(summer.result, 4)
},
// This test will take 1 second.
'Summer test asynchronous #2, done at t=2 seconds': async function(jazil) {
let summer = new Summer
await summer.AddAsync(2, 500)
await summer.AddAsync(2, 500)
jazil.ShouldBe(summer.result, 4)
},
})
jazil.AddTestSet(mainPage, 'Main page tests', {
'The main page should list the correct answer, done at t=6 seconds': async function(jazil) {
// This test runs after 2 seconds, while the main page's answer
// will be ready after 6 seconds. The answer is thus not yet
// known right now, so this should be a fair polling test.
let resultElement = jazil.testDocument.getElementById('result')
let waitingForResult
let resultText
let result
let milliSecsWaited = 0
do {
resultText = resultElement.textContent
waitingForResult =
resultText == '?' && // result not known yet
milliSecsWaited < 10000 // give up after 10 seconds
if (waitingForResult) {
await new Promise(resolve => setTimeout(resolve, 100))
milliSecsWaited += 100
}
} while (waitingForResult)
result = parseInt(resultText)
jazil.ShouldBe(result, 10)
}
})