r-lib / mockery

A mocking library for R.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

mockery::mock() can't easily mock functions which defuse their arguments

FlorianMyronStork opened this issue · comments

Hello guys,

I've encounteres the issue, that a mock-object, created by mockery::mock() can't handle functions which defuse their arguments:

my_function <- function(...) {
  dots <- rlang::enexprs(...)
  print(dots)
}

my_function(some_none_object)
# [[1]]
# some_none_object

my_wrapper <- function(...) {
  my_function(...)
}

my_wrapper(some_none_object)
# [[1]]
# some_none_object

If I now test my function without any arguments, the test passes without failure.

describe("my_wrapper", {
  it("calls my_function", {
    mock_my_function <- mockery::mock("it does!")
    mockery::stub(my_wrapper, "my_function", mock_my_function)

    expect_equal(my_wrapper(), "it does!")
    mockery::expect_called(mock_my_function, 1)
  })
})
# No issues here, as no arguments were passed via ...

But, I test it with an argument given, I get this error.

describe("my_wrapper", {
  it("calls my_function", {
    mock_my_function <- mockery::mock("it does!")
    mockery::stub(my_wrapper, "my_function", mock_my_function)

    expect_equal(my_wrapper(some_none_object), "it does!")
    mockery::expect_called(mock_my_function, 1)
  })
})
# -- Error (Line 6): my_wrapper: calls my_function -------------------------------
#   Error in `my_function(...)`: object 'some_none_object' not found
# Backtrace:
#   1. testthat::expect_equal(my_wrapper(some_none_object), "it does!")
# 4. drkleinrkprovisionentool::my_wrapper(some_none_object)
# 5. mockery my_function(...)
# at drkleinrkprovisionentool/R/test_mockery.R:11:2

The reason for this error might be found in mockery::mock

function (..., cycle = FALSE, envir = parent.frame()) 
{
    stopifnot(is.environment(envir))
    return_values <- eval(substitute(alist(...)))  # <- right here. It does not defuse arguments.
    return_values_env <- envir
    call_no <- 0
    calls <- list()
[...]

My workaround / solution to this is atm something like the following. I must admit, that I am not 100% familiar with the whole rlang:expr, rlang::enexpr, rlang::exprs, etc... family of functions yet, but it does work:

describe("my_wrapper", {
  it("calls my_function", {
    mock_my_function <- mockery::mock("it does!")
    mockery::stub(my_wrapper, "my_function", function(...) {
      dots <- rlang::exprs(...)
      mock_my_function(rlang::expr((!!!dots)))
    })

    expect_equal(my_wrapper(some_none_object), "it does!")
    mockery::expect_called(mock_my_function, 1)
    mockery::expect_args(mock_my_function, 1, quote((some_none_object)))
  })
})
# Test passed

Even though, my expectations must be adopted a little bit to the workarround...

Might there be a solution to this?

Maybe in the form of a 2nd mock() funtcion, which uses defusing operators instead of substitute? Or an optional argument like:
use_tidy_semantics = TRUE ?

That would be great! :)

We now recommend using testthat::local_mocked_binding() instead, which doesn't have this problem.