Movatterモバイル変換


[0]ホーム

URL:


Testing modules

Konrad Rudolph

2025-11-25

Testing is crucial

Writing tests to ensure code correctness is a crucial part ofdeveloping robust software. This is especially true for dynamiclanguages such as R, which lack some of the tools that ensurecorrectness in statically checked programming languages.

‘box’ does not dictate a specific style of testing, and differentkinds of testing are appropriate in different situations. However, thefollowing article suggests a structure for the test accompanying amodule which I have found to work well.

Following the suggested structure causes the implementation and thetests of each module to be located in paths next to each other. Thisstructure is found in several other conventions across programminglanguages; but it differs somewhat from the convention of R packages(where all implementation code is in a separate directory from alltesting code), and differs drastically from the convention found forinstance in Java. That said, it should be possible to make a Java-liketest project work with ‘box’, too.

Support for existing testing frameworks

‘box’ is agnostic regarding the testing framework. The followingexample employs the widely used ‘testthat’ unit testing package,but other frameworks exist, and should also work.

This example uses thebio/seqmodule from theGetting startedvignette. To enable unit testing, this module contains the followingcode at the very end:

if (is.null(box::name())) {    box::use(./`__tests__`)}

This allows us to run the tests by running the module source codefrom the command line, e.g. via

Rscript bio/seq.r

… or inside an IDE such as RStudio by choosing the menu item “Tools”› “Jobs” › “Start Local Job…”1. This works becausebox::namereturns the name of the module from which this function is called. Butif the function is invoked from code that wasn’t loaded viabox::use (as is the case here), its value isNULL. In other words,is.null(box::name()) isa way of testing whether the code currently being run is loaded as amodule, or executed directly.

The code inside theif imports the__tests__ submodule: that’s where we put the unit tests.The name__tests__ is a convention. You’re free to choose adifferent name, but I recommend sticking with this convention. Note that__tests__ is not a valid R variable name; that’s why itneeds to be written in backticks, i.e. the qualified local module nameis./`__tests__`.

In this case, the__tests__ submodule consists of adirectory with the following contents:

The__init__.r file corresponds closely to the filetests/testthat.R in a standard R package structure. Itloads ‘testthat’ and launches the tests:

box::use(testthat[...]).on_load=function (ns) {test_dir(box::file())}box::export()

This first loads and attaches the ‘testthat’ package. Althoughattaching is not strictly necessary, ‘testthat’ code is a lot morereadable without cluttering the code with explicit namequalifications.

Next it invokestest_dir and passes the tests’ directoryviabox::file inside the module’s.on_loadhook — remember that only declarations should be at file level inside amodule! All code execution should happen inside functions.

Thehelper-module.r file is a ‘testthat’ helper; theseare sourced automatically by ‘testthat’ in the environment where thetests are run. We use this mechanism to load our module in the testenvironment:

box::use(../seq[...])

And, again, attaching isn’t strictly necessary here. Note also that,depending on how the tests are run, this helper file might not beneeded, since executing the module script file itself already loads themodule contents into the global namespace; however, not all ways ofloading the tests do this; for instance, RStudio’s “Start Local Job”doesn’t. So having this helper is sometimes necessary, and neverhurts.

With this set-up, the actual unit test files look exactly likeregular ‘testthat’ test files. For instance, here’s__tests__/test-seq.r:

test_that('valid sequences can be created', {expect_error((s =seq('GATTACA')),NA)expect_true(is_valid(s))expect_error(seq('cattaga'),NA)})test_that('invalid sequences cannot be created', {expect_error(seq('GATTXA'))})

A note on RStudio and other IDEs

Most IDEs have an option to “Source” a local file. When doing this itmayseem as if the tests are correctly run, but this isn’tactually the case! This is becausebox::use doesn’t reloadcode that has already been loaded previously; instead, it uses analready loaded, cached version. This means that running the tests viathe “Source” button risks running an outdated version of the tests, orthe module, or both, after modifying their code.

To avoid this, always execute the test module in a new R session. InRStudio, the easiest way of doing this is by running it as a job, viathe menu “Tools” › “Jobs” › “Start Local Job…” (or using the option“Source as Local Job…” in the “Source” drop-down).

Test interfaces, not implementation details

One big difference between testing module code and testing packagecode is that, with the testing structure laid out above, the testingcode only sees the module’s public interface, it does not get access tothe internal module implementation.2

This isby design: the idea is that we want to test theobservable behaviour rather than the (purely incidental)current implementation, which might be changed. This is the way unittesting works in many other environments, and how it is oftenrecommended.

But I realise that this may not always be appropriate. Sometimes weneed to test implementation details. There are essentially twoworkarounds for this. For the moment, I have not yet developed a strongpreference for either of these methods.

  1. Put the tests into the module files themselves.

    # mymod.rthis_works=function ()TRUEif (is.null(box::name())) {    box::use(testthat[...])# Define teststest_that('implementation detail X works', {expect_true(this_works())    })# Invoke teststest_file(box::file())}
  2. Get access to the private module namespace for testing. Whenloading a module withbox::use, the module object has anattributenamespace that holds the private modulenamespace. It generally shouldn’t be accessed by client code, but accessby the testing code for a module is entirely legitimate:

    # __tests__/test-something.rbox::use(../mymod)impl=attr(mymod,'namespace')test_that('implementation detail X works', {expect_true(impl$this_works())})

  1. It may be tempting to instead use the “Source” buttonbut doing so is problematic; see the section “A note on RStudio andother IDEs”!↩︎

  2. Depending on how it’s invoked; but we shouldn’t makeassumptions. In particular, when invoked as a job in RStudio, the testmodule is loaded via the test helper, and thus the module internals arenot visible.↩︎


[8]ページ先頭

©2009-2025 Movatter.jp