3 min read

A testing pattern: adding switches to your code

Sometimes, testing gets hard. For instance, you’d like to test for the behavior of your function in the absence of an internet connection, or in an interactive session, without actually cutting off the internet, or from the safety of a definitely non interactive R session for tests. In this post we shall present a not too involved pattern to avoid very complicated infrastructure, as a complement to mocking in your toolbelt.

Many thanks to Hugo Gruson for very useful feedback on this post, and to Mark Padgham for his words of encouragement!

The pattern

Say my package code displays a message “No internet! Le sigh” when there’s no internet, and I want to test for that message.

First, I create a function called is_internet_down(). It could simply call curl::has_internet(). I will use it from my code.

is_internet_down <- function() {
  !curl::has_internet()
}

my_complicated_code <- function() {
  if (is_internet_down()) {
    message("No internet! Le sigh")
  }
  # blablablabla
}

Now in tests, I can’t catch the message if there is internet.

test_that("my_complicated_code() notes the absence of internet", {
  expect_message(my_complicated_code(), "No internet")
})

This is where I add a switch to my code!

is_internet_down <- function() {

  if (nzchar(Sys.getenv("TESTPKG.NOINTERNET"))) {
    return(TRUE)
  }

  !curl::has_internet()
}

my_complicated_code <- function() {
  if (is_internet_down()) {
    message("No internet! Le sigh")
  }
  # blablablabla
}

Now, when the environment variable “TESTPKG.NOINTERNET” is set to something, anything, my function is_internet_down() will return TRUE and my code will show the message. Note that I tried to name the code switch to something readable.

In the tests, I add a call to withr1 to set that environment variable for the test only.

test_that("my_complicated_code() notes the absence of internet", {
  withr::local_envvar("TESTPKG.NOINTERNET" = "blop")
  expect_message(my_complicated_code(), "No internet")
})

That’s all there is to the pattern. You could use an option and withr::local_options() instead.

Use of the pattern in the wild

A popular example of a function with an escape hatch is rlang::is_interactive().

Interactivity/internet connection are two obvious use cases, but you could use the pattern to “mock” many other things.

You could also use it as a complement or alternative to the transform argument of testthat::expect_snapshot(), for instance tweaking your code to never show something random when run inside testthat.

What about mocking instead?

Mocking consists in modifying the behavior of a function from outside, replacing it with a mock (how in mockery::stub()) for a given context (where in mockery::stub()).

For our previous example we would use:

mockery::stub(
  where = my_complicated_code,
  what = "is_internet_down", 
  how = TRUE
)
is_internet_down()[1] FALSE
my_complicated_code()No internet! Le sigh

How to choose between escape hatches and mocking? On the one hand, mocking feels tidier as the code does not need to be modified for the tests. On the other hand, mocking can very quickly get cumbersome and hard to reason about (what function has been replaced? where?) – for you now, for you in a few days, for external collaborators; that could make your codebase harder to work with.

In summary, you can pick whichever strategy you want, but don’t be afraid to choose the simpler pattern.

Conclusion

In this post we presented a solution where, to simplify testing, you add an escape hatch to your code. It might feel a bit like cheating but can sometimes be useful! Do you use this pattern? Do you have other testing “tricks” to report?


  1. You can choose to use either the withr::with_ or withr::local_ functions. Note that the withr::with_ functions will take up more space and add more nesting, though. ↩︎