Switching testthat editions and how it affects testing functions and formulas

[This article was first published on R – Statistical Odds & Ends, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

testthat is a popular R package used for unit testing. From v3.0.0, testthat introduces the idea of “editions”. This is testthat‘s way of maintaining backward compatibility. At the time of writing, the 3rd edition is the latest and incorporates the package developer’s latest recommendations, some of which could be backward incompatible. If the user wants testthat‘s old behavior, they can use earlier editions of the package.

Use edition_get() to find out which edition of testthat is currently active, and use local_edition() to change the active edition:

library(testthat)

edition_get()  # your return value may be different
# [1] 2

local_edition(3)
edition_get()
# [1] 3

You can read more about the changes that the 3rd edition introduces in Reference 1. In the rest of this post, I’ll cover one difference between the 2nd and 3rd edition: how testthat handles the environment for functions and formulas. (This was how I stumbled upon the concept of testthat editions in the first place.)

The 2nd edition ignores the environment of functions and formulas when testing for equality, while the 3rd edition takes them into account. Here’s a simple, albeit contrived, example that demonstrates this difference. Consider the following functions:

f <- function(x) {
  g <- function(x) x
  g
}

actual_value <- f()
expected_value <- function(x) x

Both actual_value and expected_value are simply the identity function: they return whatever is passed. Let’s see what happens when we test for equality in the 2nd and 3rd editions:

local_edition(2)
expect_equal(actual_value, expected_value)  # passes

local_edition(3)
expect_equal(actual_value, expected_value)
# Error: `actual_value` (`actual`) not equal to `expected_value` (`expected`).
# 
# `attr(environment(actual), 'handlers')` is absent
# `attr(environment(expected), 'handlers')` is a list
# 
# `environment(actual)` is <env:0x7fa8e59b7228>
# `environment(expected)` is <env:global>

The 2nd edition ignores the environments of the two functions and declares them as equal. The 3rd edition notices that the environments of the functions are not the same and throws an error.

You can pass the argument ignore_function_env = TRUE if you don’t want the 3rd edition to test for equality of function environments:

local_edition(3)
expect_equal(actual_value, expected_value,
             ignore_function_env = TRUE)  # passes

Here’s an example where testing for equality of function environments matters:

f <- function(x) {
  y <- 3
  g <- function(x) x + y
  g
}

actual_value <- f()
expected_value <- g <- function(x) x + y

The 2nd edition test passes while the 3rd edition test does not:

local_edition(2)
expect_equal(actual_value, expected_value)  # passes

local_edition(3)
expect_equal(actual_value, expected_value)
# Error: `actual_value` (`actual`) not equal to `expected_value` (`expected`).
# 
# `attr(environment(actual), 'handlers')` is absent
# `attr(environment(expected), 'handlers')` is a list
# 
# `environment(actual)` is <env:0x7fa8e15eb6a0>
# `environment(expected)` is <env:global>

In this case, the test should fail. When actual_value is called, it pulls the value of y from the enclosing scope, which is the body of f. When expected_value is called, it pulls the value of y from its enclosing scope, which is the global environment. As the code snippet below shows, these values can be different:

y <- 1
expected_value(10)
# [1] 11
actual_value(10)
# [1] 13

The situation is similar for formulas: the 3rd edition will check the formula’s environment while the 2nd edition won’t. Turn off the environment checks by passing ignore_formula_env = TRUE:

f <- function(x, y) {
  formula_string <- paste(y, "~", x)
  as.formula(formula_string)
}

actual_value <- f("x", "y")
expected_value <- as.formula("y ~ x")

local_edition(2)
expect_equal(actual_value, expected_value)  # passes

local_edition(3)
expect_equal(actual_value, expected_value)
# Error: `actual_value` (`actual`) not equal to `expected_value` (`expected`).
# 
# `attr(attr(actual, '.Environment'), 'handlers')` is absent
# `attr(attr(expected, '.Environment'), 'handlers')` is a list
# 
# `attr(actual, '.Environment')` is <env:0x7fa8e559b4b8>
# `attr(expected, '.Environment')` is <env:global>

local_edition(3)
expect_equal(actual_value, expected_value,
             ignore_formula_env = TRUE)  # passes

While I understand why one might want to check the environment for functions and formulas during testing, I admit that the examples above seems quite contrived. I would love to hear of examples that others have encountered where having ignore_function_env = FALSE or ignore_formula_env = FALSE was crucial.

References:

  1. Wickham, H. testthat 3e.

To leave a comment for the author, please follow the link and comment on their blog: R – Statistical Odds & Ends.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)