Documentation examples and tests are similar in some ways:
They are self-contained pieces of code.
They should cover the software’s most important functions and typical uses.
They should be simple and clear: complex examples are hard for users to understand, and complex test code can introduce testing bugs.
This similarity makes it attractive to use “doctests”, which combine
tests and documentation. Indeed, several languages, including Python and
Rust, have doctests built in.1 R also checks for errors in examples when
running R CMD check
.
The doctest package extends this idea. It lets you write testthat tests, by adding tags to your roxygen documentation. This helps you check that your examples do what they are supposed to do.
Here’s some roxygen documentation for a function:
#' Fibonacci function
#'
#' @param n Integer
#' @return The nth Fibonacci number
#'
#' @doctest
#'
#' @expect type("integer")
#' fib(2)
#'
#' n <- 6
#' @expect equal(8)
#' fib(n)
#'
#' @expect warning("not numeric")
#' fib("a")
#'
#' @expect warning("NA")
#' fib(NA)
fib <- function (n) {
if (! is.numeric(n)) warning("n is not numeric")
...
}
Instead of an @examples
section, we have a
@doctest
section.
This will create tests like:
# Generated by doctest: do not edit by hand
# Please edit file in R/<text>
test_that("Doctest: fib", {
# Created from @doctest for `fib`
# Source file: <text>
# Source line: 7
expect_type(fib(2), "integer")
n <- 6
expect_equal(fib(n), 8)
expect_warning(fib("a"), "not numeric")
expect_warning(fib(NA), "NA")
})
The .Rd file will be created as normal, with an example section like:
\examples{
fib(2)
n <- 6
fib(n)
fib("a")
fib(NA)
}
Install doctest from r-universe:
install.packages("doctest", repos = c("https://hughjonesd.r-universe.dev",
"https://cloud.r-project.org"))
Or from CRAN:
install.packages("doctest")
Or get the development version:
devtools::install("hughjonesd/doctest")
To use doctest in your package, alter its DESCRIPTION file to add the
dt_roclet
roclet and "doctest"
package to
roxygen:
Roxygen: list(roclets = c("collate", "rd", "namespace",
"doctest::dt_roclet"), packages = "doctest")
Then use roxygen2::roxygenize()
or
devtools::document()
to build your package
documentation.
Here’s a simple workflow to start using doctest:
Alter your package DESCRIPTION as above.
In your roxygen documentation, replace @examples
by
@doctest
.
In the package directory run roxygen2::roxygenize()
or devtools::document()
to create documentation. You should
see Rd files created as normal in the man/
directory,
including \examples
sections.
Add @expect
tags to your @doctest
sections.
Run roxygenize()
again. You will now see new files
created in the tests/testthat
directory, with the name
test-doctest-<topic name>.R
.
Run devtools::test()
and check that your tests
pass.
At present, you can’t use doctest from the RStudio keyboard shortcut
Ctrl + Shift + D
, because this always uses the standard
roxygen2 roclets. However, you can bind the RStudio addin “Devtools:
document a package” to a keyboard shortcut. This will use the roclets
from your package DESCRIPTION file.
You don’t need to add doctest as a dependency to your package. Just
like roxygen2 itself, you can use it to create help files and tests
without it being installed for users. However, you may wish to add it in
Suggests:
, to help other developers working on the
package:
usethis::use_package("doctest", type = "Suggests")
Don’t use @doctest
and @examples
in the
same topic. That won’t work.
Doctest currently ignores \dontrun
and
\donttest
macros. Potentially, that could lead to dangerous
code being included in tests. To avoid this, use the @omit
tag.
Each @doctest
section should include a complete
self-contained example, that would work inside a test_that
expression. Don’t rely on variables from a previous
@doctest
.
You can include expectations within e.g. if
blocks
or for
loops. Don’t forget that each roxygen tag must be
indented with a single space:
#' # Right:
#' if (TRUE) {
#' @expect equals(4)
#' 2+2
#' }
#' # Wrong:
#' if (TRUE) {
#' @expect equals(4)
#' 2+2
#' }
Tests and documentation are similar, but not identical. Tests need to cover difficult corner cases. Examples need to convey the basics to the user. I like the following advice:
… write the best possible documentation, and [R] makes sure the code samples in your documentation actually compile and run [and do what they are supposed to do]
Programming Rust, Blandy, Orendorff and Tindall, 2021
In particular, use doctest as an addition to manually
created tests, not a substitute for them. Use doctest to make
sure your examples do what they expect, and for simple tests of basic
functionality. If it’s hard to specify what to test for, consider using
@snap
to capture output:
#' @snap
summary(model)
For more complex test cases, write a test file manually.
To see an example of using the doctest package in “production”, check
out vignette("conversion")
.