Экологические проблемы с помощью testthat

У меня есть некоторые деликатные проблемы со средами, которые в настоящее время проявляются в моих модульных тестах. Моя основная структура такова

  • У меня есть основная функция main, у которой много аргументов.
  • wrapper — это функция-оболочка (одна из многих), которая относится только к выбранным аргументам main.
  • helper — это промежуточная вспомогательная функция, которая используется всеми функциями-оболочками.

Я использую eval и match.call() для плавного перехода между обертками и основной функцией. Моя проблема сейчас в том, что мои тесты работают, когда я запускаю их построчно, но не использую test_that().

Вот MWE, который показывает проблему. Если вы пройдете строки в тесте вручную, тест будет пройден. Однако при оценке всего фрагмента test_that() тест не пройден, потому что один из аргументов не может быть найден.

library(testthat)

wrapper <- function(a, b) {
  fun_call <- as.list(match.call())
  ret <- helper(fun_call)
  return(ret)
}

helper <- function(fun_call) {
  fun_call[[1]] <- quote(main)
  fun_call <- as.call(fun_call)
  fun_eval <- eval(as.call(fun_call))
  return(fun_eval)
}

main <- function(a, b, c = 1) {
  ret <- list(a = a, b = b, c = c)
  return(ret)
}

test_that("Test", {
  a <- 1
  b <- 2
  x <- wrapper(a = a, b = b)
  y <- list(a = 1, b = 2, c = 1)
  expect_equal(x, y)
})

С некоторой уверенностью я подозреваю, что мне нужно изменить среду по умолчанию, используемую eval (то есть parent.frame()), но я не уверен, как это сделать.


person hejseb    schedule 28.04.2021    source источник
comment
Определение помощника в теле оболочки может сработать.   -  person SmokeyShakers    schedule 28.04.2021


Ответы (1)


Вы хотите оценить свой вызов в родительской среде, а не в локальной функциональной среде. Измените своего помощника на

helper <- function(fun_call) {
  fun_call[[1]] <- quote(main)
  fun_call <- as.call(fun_call)
  fun_eval <- eval.parent(fun_call, n=2)
  return(fun_eval)
}

Это предполагает, что helper всегда вызывается внутри wrapper, который вызывается откуда-то еще, параметры которого определены.

Непонятно в данном случае, нужна ли вам вся эта нестандартная оценка. Вы также можете рассмотреть такое решение, как

wrapper <- function(a, b) {
  helper(mget(ls()))
}
helper <- function(params) {
  do.call("main", params)
}

Здесь wrapper просто объединяет все значения параметров в список. Затем вы можете просто передать список параметров в helper, а do.call передаст этот список в качестве параметров вашей функции main. Это оценит параметры wrapper, когда вы его вызовете, вам не нужно беспокоиться о среде выполнения.

person MrFlick    schedule 28.04.2021
comment
Кажется, это работает нормально, но я озадачен, почему! По умолчанию в eval.parent() используется n=1, а eval.parent(expr, n) — это сокращение от eval(expr, parent.frame(n)). Итак, eval.parent(expr, 1) — это eval(expr, parent.frame(1)), но среда по умолчанию в eval — это parent.frame() --- а по умолчанию для parent.frame()n=1! Итак, тл; Д-р: разве eval.parent(expr) не то же самое, что eval(expr)? (Очевидно, что нет, но я не понимаю, почему.) - person hejseb; 29.04.2021
comment
Кроме того, я только что понял, что это ломается, если в тесте вы устанавливаете a2 <- 1, а затем x <- wrapper(a = a2, b = b). Итак, кажется, что это решение требует, чтобы входные данные назывались так же, как аргументы? - person hejseb; 29.04.2021
comment
@hejseb, во-первых, важно отметить, что когда вы передаете параметры функциям, они оцениваются в вызывающей среде, но когда у вас есть значение параметра по умолчанию, они выполняются в другой среде функции. Таким образом, parent.frame ведет себя по-разному при передаче функции по сравнению со значением параметра по умолчанию. Если помощник всегда вызывается из weapper, то вы можете сделать fun_eval <- eval.parent(fun_call, n=2) Или было бы еще проще просто оценить параметры в wrapper перед переходом к helper. Есть ли причина, по которой вы откладываете оценку? - person MrFlick; 29.04.2021
comment
Спасибо, я многому здесь учусь. wrapper всегда вызывает helper, так что можно с уверенностью предположить. Тем не менее, я бы не сказал, что я намеренно задерживаю оценку --- это просто то, что я испытал. Изначально мне нужна была куча обёрток, которые просто модифицировали бы определённые аргументы main. Затем я остановился на match.call(), чтобы избежать большого количества жестко запрограммированных оболочек, и поскольку все обертки структурированы одинаково, я поместил этот материал в helper. - person hejseb; 29.04.2021
comment
Я также могу добавить, что в моем случае wrapper также имеет значения по умолчанию, и мне нужно отслеживать те и те, которые были переопределены. Этого я добиваюсь, используя formals() и передавая его также helper, поэтому я считаю, что промежуточная функция необходима (хотя она привела к некоторым неожиданным осложнениям!). - person hejseb; 29.04.2021
comment
Это не похоже на ваш вариант использования, что вы должны использовать match.call. Более прямым методом может быть что-то вроде: wrapper <- function(a, b) {helper(mget(ls()))}; helper <- function(params) {do.call("main", params)} - person MrFlick; 29.04.2021
comment
Это, правда, кажется проще. Я посмотрю, подходит ли это для моего случая. Большое спасибо! - person hejseb; 29.04.2021