When you write a package in R, you probably write tests. A popular package for unit tests with R is testthat,1 with the final invocation:
devtools::test()
If your package also includes native code, let’s say, C++, you can test it by testing the R API that exercises the native functions. But what if native functions do not map exactly to the R API, R functions use a combination of the native functions, or you want to check the results of native functions that do not map easily to R types or would require to write tedious converters?
In sxpdb, a database to efficiently store R values when instrumenting the R interpreter with R-dyntrace, I have a find_na
function that tests for the presence of NA
in a serialized R value in an efficient way. It does no try to deserialize the value, so does not allocate new memory, but traverses the serialized value in search of NA
.
I suspected this function to be defective after problems in search indexes that use this function. It does not make sense to create a specific R function to call it as it only processes serialized R values that are not - and should not - be exposes to the R API. Thus I chose to test it directly on the C++ side.
I could use another C++ testing library, such as GoogleTest but then I will have two separate test suites to run. How inconvenient, no more just running devtools::test()
!
Fortunately, testthat can integrate with catch2, another popular C++ unit test library.
Using catch2 with testthat is easy with use_catch
, which sets up all the infrastructure.
Let’s see how it looks like in practice with sxpdb
.
catch2 within testthat
I used use_catch
to build the scaffolding. For instance, it created a file tests/testthat/test-cpp.R
for testthat
to know about the C++ tests, and a file src/test-example.cpp
where to write the actual C++ tests. I renamed it to tests.cpp
.
I do not use dynamic registration in sxpdb
for performance reason, and to hide the symbols I do not want to be part of the public API. The documentation of use_catch
is less clear about what to do in that case. If you do nothing, the compilation will fail as the linker cannot find the entry point of the tests, function run_testthat_tests
.
If you do not use dynamic symbol lookup in your package, in your init.c
file, you have:
void R_init_sxpdb(DllInfo* dll) {
R_registerRoutines(dll, NULL, callMethods, NULL, NULL);
R_useDynamicSymbols(dll, FALSE);// FALSE here
}
Disabling dynamic registration hides symbols not explicitly registered in callMethods
in init.c
More information in the official documentation
testthat expects to see symbol run_testthat_tests
in your package. You have to add it to callMethods
explicitly and make it visible in the init.c
file:
extern SEXP run_testthat_tests(SEXP use_xml_sxp);
static const R_CallMethodDef callMethods[] = {
{"run_testthat_tests", (DL_FUNC) &run_testthat_tests, 1},
{NULL, NULL, 0} // Must have at the end
};
Writing your tests
Adding a test is as easy as creating a context and using the same-looking function as in testthat
:
context("Index tests") {
test_that("find_na on sexp_view") {
Serializer ser(64);
// Scalar NA real
SEXP scalar_na = PROTECT(Rf_ScalarReal(NA_REAL));
sexp_view_t sexp_view = Serializer::unserialize_view(ser.serialize(scalar_na));
UNPROTECT(1);
expect_true(find_na(sexp_view));
...
The C++ tests now appear as any other contexts:
> devtools::test()
ℹ Loading sxpdb
ℹ Testing sxpdb
✔ | F W S OK | Context
✔ | 4 | Index tests
✔ | 25 | db [0.5s]
✔ | 7 | merge [0.2s]
✔ | 24 | query [0.6s]
══ Results ══════════════════════════════
Duration: 1.7 s
[ FAIL 0 | WARN 0 | SKIP 0 | PASS 60 ]
>
And actually, that find_na
worked perfectly and the bug originated from elsewhere, but it was fun to play with catch2
!