3

我编写了一个从 aws s3-bucket 获取单个文件的导入函数。

该函数本身是一个包装器aws.s3::s3read_using(),它将读取函数作为其第一个参数。

我为什么要绕圈aws.s3::s3read_using()?因为我需要做一些特殊的错误处理,并希望包装函数做一些Recall()达到极限......但那是另一回事。

现在我已经成功构建并测试了我的包装功能,我想做另一个包装:

我想在我的包装器上迭代 n 次以将下载的文件绑定在一起。我现在很难将 'reading_function'FUN交给aws.s3::s3read_using().

我可以通过简单地使用...- 但是!我想向包装器的 USER 说明,他需要指定该参数。

因此,我决定使用 rlangsrlang::enexpr()来捕获参数并将其交给我的第一个包装器 via !!- 作为回报,它再次捕获该参数rlang::enexpr()并最终将其交给aws.s3::s3read_using()viarlang::expr(aws.s3::s3read_using(FUN = !!reading_fn, object = s3_object))

这工作得非常好和顺利。我的问题是使用testthat和测试该函数构造mockery

这是一些广泛简化的代码:

my_workhorse_function <- function(fn_to_work_with, value_to_work_on) {
  fn <- rlang::enexpr(fn_to_work_with)
  # Some other magic happens here - error handling, condition-checking, etc...
  out <- eval(rlang::expr((!!fn)(value_to_work_on)))
}

my_iterating_function <- function(fn_to_iter_with, iterate_over) {
  fn <- rlang::enexpr(fn_to_iter_with)
  out <- list()
  for(i in seq_along(iterate_over)) {
    out[[i]] <- my_workhorse_function(!!fn, iterate_over[i])
  }
  return(out)
}

# Works just fine
my_iterating_function(sqrt, c(9:16))

现在,进行测试:

# Throws an ERROR: 'Error in `!fn`: invalid argument type'
test_that("my_iterating_function iterates length(iterate_over) times over my_workhorse_function", {
  mock_1 <- mockery::mock(1, cycle = TRUE)
  stub(my_iterating_function, "my_workhorse_function", mock_1)
  expect_equal(my_iterating_function(sqrt, c(9:16)), list(1,1,1,1,1,1,1,1))
  expect_called(mock_1, 8)
})

我使用了一个workarround,但这感觉不对,即使它有效:

# Test passed
test_that("my_iterating_function iterates length(iterate_over) times over my_workhorse_function", {
  mock_1 <- mockery::mock(1, cycle = TRUE)
  stub(my_iterating_function, "my_workhorse_function", 
       function(fn_to_work_with, value_to_work_on) {
         fn <- rlang::enexpr(fn_to_work_with)
         out <- mock_1(fn, value_to_work_on)
         out})
  expect_equal(my_iterating_function(sqrt, c(9:16)), list(1,1,1,1,1,1,1,1))
  expect_called(mock_1, 8)
})

我正在使用的版本R: 4.1.1 我正在使用的版本testthat(3.1.1), mockery(0.4.2),rlang(0.4.12)

4

1 回答 1

2

我认为您在这里使事情复杂化,尽管也许我没有完全理解您的最终目标。您可以直接通过参数传递函数,没有任何问题。您上面的示例代码可以很容易地简化为(保持循环只是为了匹配您的test_that()调用):

library(testthat)
library(mockery)

my_workhorse_function <- function(fn_to_work_with, value_to_work_on) {
  fn_to_work_with(value_to_work_on)
}

my_iterating_function <- function(fn_to_iter_with, iterate_over) {
  out <- list()
  for(i in seq_along(iterate_over)) {
    out[[i]] <- my_workhorse_function(fn_to_iter_with, iterate_over[i])
  }
  return(out)
}

# Works just fine
my_iterating_function(sqrt, c(9:16))
#> [[1]]
#> [1] 3
#> 
#> ...

test_that("my_iterating_function iterates length(iterate_over) times over my_workhorse_function", {
  mock_1 <- mockery::mock(1, cycle = TRUE)
  stub(my_iterating_function, "my_workhorse_function", mock_1)
  expect_equal(my_iterating_function(sqrt, c(9:16)), list(1,1,1,1,1,1,1,1))
  expect_called(mock_1, 8)
})
#> Test passed 

您可以FUN直接通过所有嵌套函数。enexpr()除非您明确调用它们,否则您包装的函数永远不会被评估。您通常enexpr在用户提供表达式时使用,而不仅仅是函数。

于 2021-12-09T20:11:13.637 回答