About unittest, test runners and assert libraries
Zardoz
luis.panadero at gmail.com
Mon May 10 17:20:54 UTC 2021
I like to talk about the state of unit testing, test runner &
frameworks and assert libraries, as I did some little experiments
recently.
DLang, put a strong emphasis on unit testing. Language & tooling
support for unit testing (unittest blocks, assert, assert helpers
on std.exception and AssertError on core.exception, dub test ...)
However, the out of the box test runner embedded on the guts of
DLang, it's pretty limited. So, many test runner has been write,
as can bee see on dub package repository. This by itself, isn't
bad. Having several test runner to choose it's good. Specially
when one test runner it's specialized on some ways that other
not.. being faster or being straightforward to use, make more
easy to integrate with IDE, etc. Some of these test runners, are
really more a test framework, as include some helper libraries
for mocking & stubing, fluent asserts, bdd, etc.
And separated of the test runners, we have some auxiliary
libraries, that implements mocking, fluent asserts, etc. Aiming
to allow to be used on agnostic way respect the test runner, or
even on the default test runner that comes with DLang.
So, I like to show what I saw trying the five most popular test
runners (Unit-threaded, Silly, Trial, DUnit and D-Unit), and a
little details that puzzles me about the different assert
libraries.
### D-Unit
D-Unit try to be a simple implementation of the xUnit Testing
Framework. It complete ignores unittest blocks and requires to
encapsulate the tests on a class, following the same pattern that
does xUnit/jUnit (but using a mixin, instead of extending from
some base class). Includes some assert helpers following the
xUnit/jUnit framework, etc. Even can generate xUnit XML report
files and differentiated between a failed test (ie. a test case
where an assertion has failed) from an errored test (ie. a test
where a uncaught exception has been raised)
However, isn't straightforward to use (dub run -c
*my-d-unit-config*), and only understand as failed test only when
it's being using his own assert helper functions. A failed assert
form any other 3rd party assert library will be marked as an
error.
### DUnit
DUnit tries to being another full framework with mocking & assert
helpers.
Sadly, it's dead. The **githup repo it's archived*, and don't see
any update since early 2020's. Also, isn't very straightforward
to use (requires some extra config on dub.json, but at least
could be run with a simple dub test). Other annoying thing, it's
that the runner iot's pretty limited. Can't list the tests, run a
single test, or show anything useful if the test run OK. Also,
the pretty print of a error/fail on a test it's implemented on
his own assert helpers. Using 3rd party assert library on it,
would be the same experience that using on the DLang embed test
runner.
### Trial
Trial aims to be a very powerful test runner. It allow to use a
config file to select the reporters (in plural), how discover
tests, policies, and test runner implementation. Also, aims to be
more easy to integrate with IDEs. Even the author had a Visual
Code extension that auto uses Trial to discover tests on a
project. Sadly this extension it's dead, as the author saw zero
interest on it.
Using directly Trial, it's really weird. I couldn't find a way to
allow it to be used as dub test or dub run on the project.
Instead, you must build the Trial executable (dub run trial does
the work), and it will find&execute the test in your project.
Also, on my case, only worked using the master branch. Because
Trial uses internally dub (compiles dub in it'self) and so, it's
strongly coupled with dub.
It's really sad about the Visual Code extension, because gives
the quick&easiest experience to run & debug tests that I saw
using DLang on a IDE.
### Silly
Silly tries to being a better test runner that it's simple to
use, could run test on multiple threads, color output,
list&filter tests.
To use, it's really the most easy & straightforward of any test
runner that I try. I simply need to add a dependency to Silly,
and run dub test. If it's being used on a library, it's
recommended to add the dependency on a on "unittest" config or
use it on a sub package or separated dub.json .
In my personal opinion, it's the best simply because it's the
most easy to use. And don't give headhaches to configure or
require special stuff to discover tests, or put tests inside
classes, etc. Gives a quick&short summary, and gives good
information when a test fails/errors.
However, there it's room for improvement seeing what Trial and
D-Unit could do. Also looks that the author it's a bit absent.
Plus, there is some issues on the gitlab repository...
### Unit-threaded
Unit-threaded shares some goodies with Silly. Like his name
pinpoints, run the tests on multiple threads. Also, aims to being
a testing framework, including hos own assertions library,
mocking helpers, sandboxing filesystem, integration testing
facilities...
However, to discover tests, requires manually generating a file
to register the tests or add a prebuildcommand to dub.json to
autogenerate it. Something very weird, when the other tests
runners can discover the test on the fly. Also, this make more
problematic run some tests only when a dub configuration it's
activated.
Another weird thing, it's that expects that 3rd party assert
libraries, use/extend from his own exception (UnitTestException)
to fail on a test. If not, it show not useful junk stack trace on
the output.
### About Asserts, assert libraries and AssertError
And this last details, makes my head to scratch, because I saw
this on many assert libraries. Every assert library, throws a
plain Exception (or UnitTestException if Unit-threaded it's added
as dependency). This make really impossible to differentiate a
failed test from an errored test (Remember, an test case where an
assertion fails, its a fail test. An test case where a uncaught
exception it's raised its an error.)
To me, sound logic that any assert library should use the same
exception that it's throw by DLang's assert(). This is
AssertError. And that tests runners should count any AssertError
(or derived) inside a test case as failed, and any other
exception as an error. This allows to assert libraries and assert
runners work better without needing to be coupled to extend from
some internal Exception implemented by a test runner and to have
code on assert libraries to handle every test runner on the wild.
So, I like to ask **WHY no body does this**. Perhaps i don't see
the inconvenient. It's a thing that puzzles me, and I think that
should be fixed. It's one of many rought edged that have DLang
and that it's pretty to fix.
I have my little experiment fork/branch of Silly doing exactly
this, and Pijamas develop branch using AssertError. The result,
it's that Silly counts correctly failed and errored tests and
Silly & Pijamas not have any coupling between.
PD : Sorry for my poor english. Also, I have a strong background
with Java and JavaScript, so I have tendency to use jUnit, Spock,
jShould, etc... as some kind of gold standard for how unit
testing should be.
More information about the Digitalmars-d
mailing list