Assert::Refute - a unified testing and assertion tool
Unit tests are great. They show that the code was actually designed to a given spec.
Runtime assertions are great. They show that the code is actually running the way it was designed when applied to real data.
Design by contract is a great concept, but it's a bit of an overkill for most projects.
However, sometimes I feel an urge to just rip several lines from a unit test and put them right into production code. Test::More doesn't help here much since my application isn't really meant to output TAP or run in a harness.
So I started out Assert::Refute to narrow the gap:
# somewhere in production code
use Assert::Refute qw(:all), { on_fail => 'carp' };
# in the middle of a large sub
refute_these {
isa_ok $some_object, "My::Type", "Correct type detection after decode_json";
is $fee + $price, $total, "Payment parts match";
like $string, qr/f?o?r?m?a?t?/, "Output is really what we expect it to be";
};
This would just run silently if everything meets the spec, and would emit a warning with TAP output included it it doesn't.
It is also worth noting that refute_these { ... }
returns a report
object (similar to Test::Builder,
but not a singleton), and that the code block also receives it as
first argument. (E.g. one can include more diagnostics if the test
is already failing).
There is also a subcontract { ... }
(not subtest) call for executing
a group of checks together.
The rest can be found in documentation.
Why refute?
A successful prove
run actually proves nothing. It's only the failures
that are meaningful! This is similar to
falsifiability
concept in
modern science: we don't prove a theory in experiment; instead, we
try hard to refute it.
Same goes for runtime assertions.
So the underlying mechanism of this module is a refute call:
$report->refute ("What exactly went wrong", "Why we care about it");
Although somewhat counterintuitive, it leads to more efficient code than a more common
ok ("Everything was ok", "Why we care about it")
or diag "What exactly went wrong";
Extending the arsenal
The package includes a builder
to create new checks/conditions. Each such condition would be able
to work under Test::More
as well, provided that Test::More
was
loaded earlier than Assert::Refute
.
There is also a small library including tests for arrays, hashes, error messages and warnings. It's currently much more scarce than Test::Most.
There is no easy way to import checks based on Test::Builder
, but
maybe one will be created in the future.
Performance impact
I was able to verify 10K refute_these
blocks with 100 passing assertions
each in 3.2 seconds on my 4500 bogomips laptop. Assert::Refute
is
optimized for happy path, for obvious reasons. There's still room
for improvement though.
Intended usage
The most obvious use case is working on legacy software where
Assert::Refute
may act as both a prototype of unit tests and
a safety net while in transition.
Still even in new code it may be feasible to know that certain invariants hold in production whatever real data is being processed by the code.
Conclusion
This module is still in development, so feel free to point out what you'd like to see there before it's too entrenched.
This is great! Essentially all of my Perl is scientific code, and assertions would be a much simpler way to test things than a full test suite. A test suite requires decoupled implementations, which is not always practical for scientific code that is used in just one or two places. For this very reason, I almost never write tests for my scientific code. Assertions would help me feel at least a little less dirty. :-)
On a closely related note, if/when it comes time to run a heavy calculation and I want to speed things up, is there a way to turn assertions into no-ops?
I think Keyword::DEVELOPMENT can do the trick.
Also you could use a compile-time constant:
Perl will optimize the if statement out because it knows DEBUG is a constant.
(Note that I already renamed refute_these to try_refute. The old name is still there but deprecated.)
Just for information.
Starting from v.0.13, Assert::Refute honors NDEBUG environment variable. If it is set to true, try_refute{...} blocks are optimized out.
Thanks for your request.