Mock Them All: Simulate to Better Test with testthat

Author : Murielle Delmotte
Categories : package, development, tips
Tags : package, testthat, unit test
Date :

Unit testing in R. You know, those small functions that ensure your code works flawlessly—even when you’re on vacation or writing new modules at 2 a.m.
But let’s be honest: when it comes to testing user interactions or external resources, things can quickly turn into a headache.
What do you do when your code requires a file selection dialog or a connection to an API that takes forever to respond?
No way you want to block everything just for a test!

In those cases, it’s time to get clever. The idea isn’t to give up on testing, but to cleverly bypass what’s problematic. That’s where a well-known technique comes in: mocking.
Luckily, R and the testthat package provide tools just for this.

Mocking means temporarily replacing a function or resource with a fake, controlled version that simulates the expected behavior.

Intrigued? Let’s dive in.

The Art of Pretending: Simulating user interactions

Imagine a function that asks the user to select a file.
If you had to test that function with a real dialog box, it would be a total waste of time—not to mention impossible to automate easily in a CI environment.
That’s where the magic of mocking comes in.

Here’s an example of such a function:

select_file <- function() {
  file_path <- choose.files()  # Cette fonction ouvre une boîte de dialogue
  cat("You have chosen the file :", file_path)
}

You don’t want to manually select a file every time you run the test.
With with_mocked_bindings() from the {testthat} package, you can replace choose.files with a version that simply returns a predefined path.
This avoids any interaction and makes the test instant:

test_that("select_file works without a dialog box", {
  with_mocked_bindings(
    code = {
      expect_output(select_file(), "You have chosen the file : C:/fakedir/fakefile.txt")
    },
    choose.files = function() {"C:/fakedir/fakefile.txt"}
  )
})

And that’s it! You test the function as if the user already selected a file—no dialog box involved.

When simulation becomes essential: Testing external resources

Unit tests are often used to verify functions that rely on external resources such as APIs, databases, or network interactions.
Imagine a function that retrieves data from an API.
Actually calling the API during every test would be slow, costly, and completely unnecessary.
Instead, you can simulate the API response. Here’s how:

fetch_data <- function() {
  response <- httr::GET("https://api.exemple.com/data")
  content <- httr::content(response)
  return(content)
}

To test this function without making a real API call, you can mock the response using with_mocked_bindings.

Here’s an example where the API call is replaced with a handcrafted response:

Now you’re testing the function without ever hitting the real API, and you have full control over what’s returned.
You can simulate different situations (successful response, error, timeout, etc.) without depending on the actual API state.

⚠️ Not everything can be mocked: The limitations

Some functions can’t be mocked directly, for example:

Sys.time()
Sys.getenv()

Trying to override them with with_mocked_bindings() results in:

Error in `local_mocked_bindings(...)`: Can't find binding for `Sys.time`

Why? Because certain functions like Sys.time() and Sys.getenv() are primitive functions in R.
They’re deeply embedded in the language core and not regular R objects you can easily override like user-defined functions.

🩹 The workaround: Encapsulate the behavior

The solution: create a wrapper function in your code that you can mock.

Example:

# Dans votre package
get_current_time <- function() { Sys.time()}
say_hello <- function() {
  if (format(get_current_time(), "%H") == "12") {
    "Time for lunch!"
  } else {
    "Just a little longer..."
  }
}

Et dans vos tests :

test_that("say_hello correctly detects noon", {
  with_mocked_bindings(
    code = expect_equal(say_hello(), "Time for lunch!"),
    get_current_time = function() as.POSIXct("2025-04-25 12:00:00")
  )
})

🚪 In summary

  • with_mocked_bindings() allows you to simulate functions whose behavior varies depending on user input, system state, or time, in an elegant way;
  • Some functions can’t be mocked directly (like Sys.time, Sys.getenv): wrap them in a separate function;
  • It’s a great way to create fast, reliable, non-interactive tests.

About the author

Murielle Delmotte

Murielle Delmotte

DATA SCIENTIST – Joueuse de R passionnée


Comments


Also read