Testing C++ Code in R Packages

With testthat and catch2

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!


  1. Other R unit testing packages are unitizeR, tinytest from the tinyverse, RUnit ↩︎

Pierre Donat-Bouillud
Pierre Donat-Bouillud
Researcher

My research interests including programming languages, fuzzing and testing.

Related