library(individual)
library(mockery)
# Utility function for printing out C++ code
lang_output <- function(x, lang) {
cat(c(sprintf("```%s", lang), x, "```"), sep = "\n")
}
cpp_output <- function(x) lang_output(x, "cpp")
print_cpp_file <- function(path) cpp_output(readLines(path))
So you've created a model, but you want to know if your processes are correct. This vignette walks you through how to mock the individual API and write unit tests to check your processes.
Let's test the rendering function from our “Modelling” vignette:
render_state_sizes <- function(api) {
api$render('susceptible_counts', length(api$get_state(human, S)))
api$render('infected_counts', length(api$get_state(human, I)))
api$render('recovered_counts', length(api$get_state(human, R)))
}
Mockery is a lovely R package which allows you to stub and spy arbitrary objects and functions.
We could test that the correct counts are being rendered like so:
human <- mockery::mock()
S <- mockery::mock()
I <- mockery::mock()
R <- mockery::mock()
api <- list(
get_state = mockery::mock( # create a function which
seq(10), #returns 1:10 on the first call
seq(5) + 10, #returns 11:15 on the second call, and
seq(2) + 15 #returns 16:18 on the third call
),
render = mockery::mock()
)
render_state_sizes(api)
mockery::expect_called(api$get_state, 3) #get_state is called three times
mockery::expect_args(api$get_state, 1, human, S) #the first time for the S state
mockery::expect_args(api$get_state, 2, human, I) #then for the I state
mockery::expect_args(api$get_state, 3, human, R) #then for the R state
mockery::expect_called(api$render, 3) #render is called three times
mockery::expect_args(api$render, 1, 'susceptible_counts', 10) #the first time for the S state
mockery::expect_args(api$render, 2, 'infected_counts', 5) #then for the I state
mockery::expect_args(api$render, 3, 'recovered_counts', 2) #then for the R state
Let's say you had a C++ version of the render function:
#include <individual.h>
#include <Rcpp.h>
//[[Rcpp::export]]
Rcpp::XPtr<process_t> create_render_process() {
return Rcpp::XPtr<process_t>(
new process_t([=] (ProcessAPI& api) {
api.render(
"susceptible_counts",
api.get_state("human", "susceptible").size()
);
api.render(
"infected_counts",
api.get_state("human", "infected").size()
);
api.render(
"recovered_counts",
api.get_state("human", "recovered").size()
);
}),
true
);
}
The mockcpp package provides a mocking framework which lets you write analogous C++ unit tests:
#include <Rcpp.h>
#include <individual.h>
#include <testthat.h>
#include <mockcpp.h>
#include "code.h"
/*
* Derive a mock api class from the ProcessAPI
* See https://github.com/rollbear/trompeloeil
*/
class MockAPI : public ProcessAPI {
public:
MockAPI() : ProcessAPI( // Initialise the ProcessAPI using some empty state
Rcpp::XPtr<State>(static_cast<State *>(nullptr), false),
Rcpp::XPtr<Scheduler<ProcessAPI>>(static_cast<Scheduler<ProcessAPI>*>(nullptr), false),
Rcpp::List(),
Rcpp::Environment()
) {};
MAKE_CONST_MOCK2(get_state, const individual_index_t&(const std::string& individual, const std::string& state), override);
MAKE_MOCK2(render, void(const std::string& label, double value), override);
};
context("State rendering") {
test_that("state rendering returns the correct counts") {
MockAPI api;
auto population_size = 20;
/*
* Check the get_state calls
*/
auto S = individual_index_t(
population_size,
std::vector<size_t>{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
);
auto I = individual_index_t(
population_size,
std::vector<size_t>{10, 11, 12, 13, 14}
);
auto R = individual_index_t(
population_size,
std::vector<size_t>{15, 16}
);
REQUIRE_CALL(api, get_state("human", "susceptible")).RETURN(S);
REQUIRE_CALL(api, get_state("human", "infected")).RETURN(I);
REQUIRE_CALL(api, get_state("human", "recovered")).RETURN(R);
/*
* Check the render calls
*/
REQUIRE_CALL(api, render("susceptible_counts", 10));
REQUIRE_CALL(api, render("infected_counts", 5));
REQUIRE_CALL(api, render("recovered_counts", 2));
/*
* Call the render process with our mocked api
*/
auto renderer = create_render_process();
(*renderer)(api);
}
}
We can then build our package and run the test to ensure our mocking works:
temp_lib <- tempdir(check = FALSE)
# Install test package
install.packages(
system.file('testcpp', package = 'individual', mustWork = TRUE),
repos = NULL,
type = 'source',
lib = temp_lib
)
# load test package
library('testcpp', lib.loc = temp_lib)
# run the test
testthat::test_file(
system.file(
'testcpp/tests/testthat/test-cpp.R',
package = 'individual',
mustWork = TRUE
)
)
#> ✔ | OK F W S | Context
#>
⠏ | 0 | C++
✔ | 1 | C++
#>
#> ══ Results ═════════════════════════════════════════════════════════════════════
#> OK: 1
#> Failed: 0
#> Warnings: 0
#> Skipped: 0
And there you go, both R and C++ processes can be unit tested by mocking the simulation API.