unitizer
simplifies creation, review, and debugging of
tests in R. It automatically stores R expressions and the values they
produce, so explicit expectations are unnecessary. Every test is easy to
write with unitizer
because testing and using a function
are the same. This encourages non-trivial tests that better represent
actual usage.
Tests fail when the value associated with an expression changes. In interactive mode you are dropped directly into the failing test environment so you may debug it.
unitizer
is on CRAN:
It bakes in a lot of contextual help so you can get started without reading all the documentation. Try the demo to get an idea:
Or check out the screencast
to see unitizer
in action.
Are you tired of the deparse
/dput
then
copy-paste R objects into test file dance, or do you use
testthat::expect_equal_to_reference
or other snapshot
testing a lot?
With unitizer
you interactively review your code as you
would when typing it at the R prompt. Then, with a single keystroke, you
tell unitizer
to store the code, and any values, warnings,
or errors it produced, thereby creating a formal regression test.
Do you wish the nature of a test failure was more immediately obvious?
When tests fail, you are shown a proper diff so you can clearly identify how the test failed:
Do you wish that you could start debugging your failed tests without additional set-up work?
unitizer
drops you in the test environment so you can
debug why the test failed without further ado:
Do you avoid improvements to your functions because that would require painstakingly updating many tests?
The diffs for the failed tests let you immediately confirm only what you intended changed. Then you can update each test with a single keystroke.
unitizer
Differ from
testthat
?unitizer
requires you to review test outputs and confirm
they are as expected. testthat
requires you to assert what
the test outputs should be beforehand. There are trade-offs between
these strategies that we illustrate here, first with
testthat
:
vec <- c(10, -10, 0, .1, Inf, NA)
expect_error(
log10(letters),
"Error in log10\\(letters\\) : non-numeric argument to mathematical function\n"
)
expect_equal(log10(vec), c(1, NaN, -Inf, -1, Inf, NA))
expect_warning(log10(vec), "NaNs produced")
And with unitizer
:
vec <- c(10, -10, 0, .1, Inf, NA)
log10(letters) # input error
log10(vec) # succeed with warnings
These two unit test implementations are functionally equivalent.
There are benefits to both approaches. In favor of
unitizer
:
In favor of testthat
:
unitizer
you
still need to unitize
and review the tests.unitizer
stores reference values in binary RDSes (see Collaborating
with Unitizer).unitizer
is particularly convenient when the tests
return complex objects (e.g as lm
does) and/or produce
conditions. There is no need for complicated assertions involving
deparsed objects, or different workflows for snapshots.
testthat
tests to unitizer
If you have a stable set of tests it is probably not worth trying to
convert them to unitizer
unless you expect the code those
tests cover to change substantially. If you do decide to convert tests
you can use the provided testthat_translate*
functions (see
?testthat_translate_file
).
unitizer
and PackagesThe simplest way to use unitizer
as part of your package
development process is to create a tests/unitizer
folder
for all your unitizer
test scripts. Here is a sample test
structure from the demo package:
unitizer.fastlm/ # top level package directory
R/
tests/
run.R # <- calls `unitize` or `unitize_dir`
unitizer/
fastlm.R
cornerCases.R
And this is what the tests/run.R
file would look
like
library(unitizer)
unitize("unitizer/fastlm.R")
unitize("unitizer/cornerCases.R")
or equivalently
library(unitizer)
unitize_dir("unitizer")
The path specification for test files should be relative to the
tests
directory as that is what R CMD check
uses. When unitize
is run by R CMD check
it
will run in a non-interactive mode that will succeed only if all tests
pass.
You can use any folder name for your tests, but if you use
“tests/unitizer” unitize
will look for files automatically,
so the following work assuming your working directory is a folder within
the package:
unitize_dir() # same as `unitize_dir("unitizer")`
unitize("fast") # same as `unitize("fastlm.R")`
unitize() # Will prompt for a file to `unitize`
Remember to include unitizer
as a “suggests” package in
your DESCRIPTION file.
unitizer
unitizer
Writes To Your FilesystemThe unitize
d tests need to be saved someplace, and the
default action is to save to the same directory as the test file. You
will always be prompted by unitizer
before it writes to
your file system. See storing
unitized
tests for implications and alternatives.
all.equal
Stored Reference
ValuesOnce you have created your first unitizer
with
unitize
, subsequent calls to unitize
will
compare the old stored value to the new one using
all.equal
. You can change the comparison function by using
unitizer_sect
(see tests
vignette).
This means you need to be careful with expressions that may deparse differently on different machines or with different settings. Unstable deparsing will prevent tests from matching their previously stored evaluations.
For example, in order to avoid round issues with numerics, it is better to use:
Instead of:
Similarly issues may arise with non-ASCII characters, so use:
Instead of:
This issue does not affect the result of running the test as that is never deparsed.
unitizer
can track and manage many aspects of state to
make your tests more reproducible. For example, unitizer
can reset your R package search path to what is is found in a fresh R
session prior to running tests to avoid conflicts with whatever
libraries you happen to have loaded at the time. Your session state is
restored when unitizer
exits. The following aspects of
state can be actively tracked and managed:
State management is turned off by default because it requires tracing
some base functions which is against CRAN policy, and generally affects
session state in uncommon ways. If you wish to enable this feature use
unitize(..., state='suggested')
or
options(unitizer.state='suggested')
. For more details
including potential pitfalls see ?unitizerState
and the reproducible tests vignette.
browser
/debug
/recover
If you enter the interactive browser as e.g. invoked by
debug
you should exit it by allowing evaluation to complete
(e.g. by hitting “c” until control returns to the unitizer
prompt). If you instead hit “Q” while in browser mode you will
completely exit the unitizer
session losing any
modifications you made to the tests under review.
Tests that modify objects by reference are not perfectly suited for
use with unitizer
. The tests will work fine, but
unitizer
will only be able to show you the most recent
version of the reference object when you review a test, not what it was
like when the test was evaluated. This is only an issue with reference
objects that are modified (e.g. environments, RC objects,
data.table
modified with :=
or
set*
).
unitizer
Is ComplexIn order to re-create the feel of the R prompt within
unitizer
we resorted to a fair bit of trickery. For the
most part this should be transparent to the user, but you should be
aware it exists in the event something unexpected happens that exposes
it. Here is a non-exhaustive list of some of the tricky things we
do:
.Last.value
will not workstdout
and stderr
during test
evaluation to capture those streams (see details
on tests vignette), though we take care to do so responsiblyunitizer
interactions do not pollute itIn particular, you should avoid evaluating tests that invoke
debug
ged functions, or introducing interactivity by using
something like options(error=recover)
, or
readline
, or some such. Tests will work, but the
interaction will be challenging because you will have to do it with
stderr
and stdout
captured…
unitize
within try
/
tryCatch
BlocksDoing so will cause unitize
to quit if any test
expressions throw conditions. See discussion in error
handling.
Some base functions are masked at the unitizer
prompt:
q
and quit
are masked to give the user an
opportunity to cancel the quit action in case they meant to quit from
unitizer
instead of R. Use Q to quit from
unitizer
, as you would from browser
.ls
is masked with a specialized version for use in
unitizer
.traceback
is masked to report the most recent error in
the order presented by the unitizer
prompt.