Table of Contents
This can be installed directly from codeberg:
npm i -D git+https://codeberg.org/harald/unittesterjs.git
For details, see the api documentation. Here is a quick overview.
This package provides the class UnitTester to run test functions while keeping
track of failures, logging them and to provide a summary. It does not
provide "assert" or "expect" functions to check conditions. There are packages
provide this, for example chai, which has no dependencies.
Lets go through a minimal example fragment:
class TestStuff {
testDogBarks(): void {
}
testCatIsNotDog(): void {
}
}
const ut = new UnitTester(...);
await ut.run(new TestStuff());
ut.summarize();
UnitTester, say ut.test.... Or configure the pattern when creating ut.ut.run(...) for the instances of your test classes. It runs all
test... methods and keeps track of sucessful, ignored, failing and crashing
ones. Depending on the logger provided when creating ut, more or less
details are logged during run().ut.summarize() to get a one-liner with counts about successful,
ignored, failed and crashed methods.Follow the general strategy above to write tests, but wrap the main code, into
document.addEventListener("DOMContentLoaded", async () => {
...
}
You may use the HtmlTestLogger instead of the DefaultTestLogger. Then load
the file into a browser with a trivial HTML file, the gist of which is:
<script type="module" src=".../path/from/html/to/MyTest.js"></script>
If the browser complains about imports like
import { assert, AssertionError } from "chai";
read about importmap on mdn;
See the examples in the source code:
Your development environment likely provides means already to run a test HTTP
server and serve the HTML file loading your test file. If not, check node or
python documentation for one liners.
The goal was to provide as little magic as possible, in particular where boring old normal programming provides already all that is needed, like for parametrized tests or magically finding test files. Read on.
Test classes may have the methods beforeEach, afterEach
and afterAll with the obvious meaning. There is no beforeAll, use the
constructor instead.
A test class may extend from a base class. Before/after and test methods of the base class are used if not overriden, just normal business.
Let the test class constructor have parameters, create instances with different
parameters and pass them ut.run(), like:
const ut = new UnitTester(...);
for(const animal of ["dog", "cat", "chicken"]) {
ut.run(new AnimalTest(animal));
}
For better identification of test instances in the logs, set the test class
field paramId to some recognizable string derived from the parameters.
The difference is defined when creating the UnitTester. The "assert" or
"expect" conditions you use likely throw a specific subclass of Error. These
are failed tests. Other Error instances count as crashed. Making the
difference is a bit gold plating, and not extremely important. If a test method
throws, things are broken and need to be fixed. Test functions which return
anything but undefined are crashed too.
These are defined by a regular expression passed to the UnitTester constructor
which defaults to /^test/. UnitTester.run() has a parameter
to override it per test instance. A matching function with more than zero
parameters is logged as a mistake and not run as a test.
A module (basically a JavaScript or TypeScript file) with a bunch of test
classes can be passed to UnitTester.runAll(). All constructor bearing
functions, typically class objects, which match /^Test/ by default, are called
with new and no parameters to create a test class instance. Each is handed to
UnitTester.run() as described above.
For better logging and statistics, it is nice to have something like
export const name = import.meta.url.split('/').splice(-2).join('/');
in a module, but this is optional.
FIXME: Running test classes in a module with constructor parameters is not implemented.
There is nothing provided to collect JavaScript files (modules) with
tests. Write a TypeScript/JavaScript file as outlined above and run it. See the
main function in our own
test
as an example.
Progress and failures are logged through a logger provided when constructing the
UnitTester. The type is TestLogger and may be completely replaced. The
default is a DefaultTestLogger which itself can be configured, see
TestLoggerOptions.
To skip a test instance, you could simply comment out the code which calls
UnitTester.run(), but this aggravates the compiler because of unused code and
you may eventually forget to uncomment the code again. Therefore we have
UnitTester.runX(). Simply change from .run() to .runX() to skip the test
instance, but have a reminder in the log output about it. The same holds for
.runAllX() as a drop-in for .runAll() to skip whole modules as you may want
during debugging.
To ignore a test function depending on some condition, use the following at the start of its body:
if (condition) {
throw new TestIgnored("this needs haxwurbel to be implemented first");
}
It will be logged as a reminder that it needs work, except if the logger is configured to not do so. If there is no condition and you only want to get rid of the test for a while, use
TestIgnored.throw("some explanation")
The effect is the same as with throw new, except that the compiler will not bother you
about unreachable code.
Because async test methods are supported, UnitTester.run() ends up being
async
too,
sorry. So your compiler or linter will likely tell you to deal with the
Promise returned, whether you have any async test at all or not.
To verify that a test can finish within some reasonably limited time, use
UnitTester.failAfterMs(). It sets up a timout to fail the promise representing
the test function after the given time. The test function counts as crashed
(see above) then. Example:
class AnimalTest {
constructor(private readonly ut: UnitTester) {}
testDogBarks(): void {
ut.failAfter(500);
await dog.barks();
}
}
This assumes that the UnitTester passed to the constructor is the one
executing the test.
There are tons of unit test frameworks for JavasScript. Their task can be partitioned into the following aspects:
assertThat("bobo").isA(Cat)it('verifies the dog can bark', ...) or just
@Test barkingDogTest() {...} which typically contain minimal setup code
and one or more boolean expressions according to (1).One of the most popular ones for Java is JUnit and it combines the first three. In the
JavaScript eco system, at least chai seems to provide only (1), while jest,
jasmine, mocha, karma and more cover the first four or even all aspects.
Lets look at the number of transitive dependendencies as of 2026-01-29, as derived with the help of npmgraph:
And I want to load it into the browser and run a few tests there, because I
don't want to include jsdom (44 dependencies) just to bascially test one or
two functions. I typically use Jasmine, but reading the "browser setup" hints, I
got somewhat annoyed.
In principle I could just use chai (zero dependencies, easy to get into the
browser without following elaborate descriptions and jumping through hoops)
and write a function like
function testitall(): void {
assert.equal(...);
assert.lengthOf(...);
...
}
Then check how and what fails. Yet, this feels somewhat too ugly. And I wanted to see what it means to write a very simple test runner. (The ultimate motivation.) This is what came out of it.
This package was once called LutruJS. As this a weird and awkward word, it was changed to the boring but straight forward UnitTesterJS.
Keywords: lutrujs unittest javascript typescript