I'm having a problem using match.fun
together with test_that
when match.fun
is used inside nested functions. To illustrate, I've built a quick toy example R package containing two functions. The latter simply calls the former:
i_dont_throw_error <- function(function_name)
match.fun(function_name)("hello")
i_throw_error <- function(function_name)
i_dont_throw_error(function_name)
I then wrote testthat
tests as follows:
test_that("Testing for an error with match.fun one level deep.",{
print_function <- function(x)
print(x)
expect_equal(i_dont_throw_error("print_function"), "hello")
})
test_that("Testing for an error with match.fun two levels deep.",{
print_function <- function(x)
print(x)
expect_equal(i_throw_error("print_function"), "hello")
})
The first test is fine, but I get an error with the second test. The output from testthat
is
==> devtools::test()
Loading testthatTest
Loading required package: testthat
Testing testthatTest
[1] "hello"
.1
1. Error: Testing for an error with match.fun two levels deep. -----------------
object 'print_function' of mode 'function' was not found
1: withCallingHandlers(eval(code, new_test_environment), error = capture_calls, message = function(c) invokeRestart("muffleMessage"))
2: eval(code, new_test_environment)
3: eval(expr, envir, enclos)
4: expect_equal(i_throw_error("print_function"), "hello") at test_test_me.R:12
5: expect_that(object, equals(expected, label = expected.label, ...), info = info, label = label)
6: condition(object)
7: compare(actual, expected, ...)
8: i_throw_error("print_function")
9: i_dont_throw_error(function_name) at C:\Users\jowhitne\Desktop\eraseMe\testthatTest/R/test_func.R:4
10: match.fun(function_name) at C:\Users\jowhitne\Desktop\eraseMe\testthatTest/R/test_func.R:1
11: get(as.character(FUN), mode = "function", envir = envir)
I don't understand why the first test passes but the second test fails. In fact, running the failing test directly from the console works just fine:
> print_function <- function(x)
+ print(x)
> i_throw_error("print_function")
[1] "hello"
I know it has something to do with the environments, but I would have expected this to work after match.fun
searches through two environments. Any idea what I'm missing here? Thanks in advance for the help.
Related questions:
I spent a few hours getting to the bottom of this issue. It is an environment issue related to how testthat evaluates expressions when run via
devtools::test()
but not when run interactively.Short version
testthat creates a number of new environments (to ensure independence of different tests and thus avoid errors from code interaction) when running tests and these don't inherit in the same way they do when you run interactively. The solution is generally to use
dynGet()
to find the object because this uses black magic to find the object (which is to say I don't understand how it works).Long version
I created a new package, test.package, based on your functions, available here and it replicates your error. I suspected it was an environment issue because I've had similar bugs in the past where I had to think hard about
get()
,parent.frame()
,parent.env()
etc. See introduction to environments in Hadley's Advanced R.Debugging stuff when not running interactively is hard. But
devtools::test()
does print warnings to the console, so I used that as my way to extract debugging information. Doing so required me to write a somewhat complicated function to help with this:The purpose of the function is basically to help print nicely formatted warnings about the environments that are searched when looking for an object. The reason I didn't just use
print()
is that this doesn't get shown in the right place in the testthat log but warnings do.First, I renamed and modified your functions to:
Thus, it now prints (as warnings) 2/3 environments and their parents when you evaluate it. The console output looks like this for
outer_v1
:Which is quite long, but it is broken into 4 parts: 3 parts that relate to the recursive printing of the environments, and the error that occurs at the end. The environments are tagged with the prefix seen in the function definition so it is easy to see what is going on. E.g.
current environment
is the current (inside the function call) environment.Going over the three lists we find these paths:
0x397a2a8
(function environment) >namespace:test.package
>0x23aa1a0
>namespace:base
>R_GlobalEnv
. None of these have the object we want i.e.print_function
.0x3d25070
(an empty environment, not sure why it is there) >0x3d25070
(has our object!) >0x3cff218
(another empty environment) >0x370c908
(one more) >namespace:test.package
>0x23aa1a0
>namespace:base
>R_GlobalEnv
.namespace:test.package
>0x23aa1a0
>namespace:base
>R_GlobalEnv
.The paths of defining/enclosing and parent frame overlap with the former being a subset of the latter. It turns out that our object is in parent.frame, but 2 steps up. Thus, we can fetch the function in this case with
get(function_name, envir = parent.frame(n = 2))
. Thus, second iteration is:This still works interactively because we added an if clause where it first tries to find it the normal way, and then if not, tries the
parent.frame(n = 2)
way.On testing via
devtools::test()
we find thatouter_v2
now works but we brokeinner_v2
though it works interactively. If we inspect the log we see:So our object is two steps up, but we still miss it. How? Well, we called it
parent.frame(n = 2)
from a different place than before and this changes something. If we replace it withparent.frame(n = 1)
it works again.So, using
parent.frame()
is not a thorough solution because one needs to know how many steps to go back up which depends on how many nested functions one has. Is there a better way? Yes.dynGet()
uses black magic to figure this out on its own (i.e. I don't know how it works). One could presumably also accomplish this by implementing a customget2()
that loops through all the possible values forn
inparent.frame()
(left as exercise to the reader).Thus, our final version of the functions are:
These pass both the interactive and
devtools::test()
tests. Hooray!