R 中具有全局变量的单元测试函数

Unit testing functions with global variables in R

序言:包结构

我有一个 R 包,其中包含一个 R/globals.R 文件,其内容如下(简体):

utils::globalVariables("COUNTS")

然后我有一个简单地使用这个变量的函数。例如,R/addx.R 包含一个将数字添加到 COUNTS

的函数
addx <- function(x) {
    COUNTS + x
}

在我的包裹上做 devtools::check() 时一切正常,没有人抱怨 COUNTS 超出了 addx() 的范围。

问题:编写单元测试

但是,假设我还有一个包含以下内容的 tests/testthtat/test-addx.R 文件:

test_that("addition works", expect_gte(fun(1), 1))

测试的内容在这里并不重要,因为当 运行 devtools::test() 我得到一个“找不到对象 'COUNTS'” 的错误。

我错过了什么?我怎样才能正确编写这个测试(或设置我的包)。

我尝试解决的问题

  1. 在函数定义之前、内部或之后将 utils::globalVariables("COUNTS") 添加到 R/addx.R
  2. 在我能想到的所有地方将 utils::globalVariables("COUNTS") 添加到 tests/testthtat/test-addx.R
  3. 在我能想到的tests/testthtat/test-addx.R的所有地方手动初始化COUNTS(例如,使用COUNTS <- 0<<- 0)。
  4. 正在阅读 GitHub 上使用类似语法 (source) 的其他包中的一些示例。

我认为您误解了 utils::globalVariables("COUNTS") 的作用。它只是声明了COUNTS是一个全局变量,所以当代码分析看到

addx <- function(x) {
    COUNTS + x
}

它不会抱怨使用未定义的变量。但是,实际创建变量取决于您,例如通过显式

COUNTS <- 0

源代码中的某处。我想如果你这样做,你甚至不需要 utils::globalVariables("COUNTS") 调用,因为代码分析将看到全局定义。

当您进行一些非标准评估时,您会需要它,因此变量的来源并不明显。然后你把它声明成一个全局的,代码分析就不用管它了。例如,您可能会收到关于

的警告
subset(df, Col1 < 0)

因为它似乎使用了一个名为 Col1 的全局变量,但这当然没关系,因为 subset() 函数以非标准方式求值,让您包含列名而无需编写 df$Col.

@user2554330 的回答对很多事情都很好。

如果我没理解错的话,你有一个 COUNTS 需要更新,所以把它放在包环境中可能是个问题。

您可以使用的一种技术是使用本地环境。

两种选择:

  1. 如果它总是在一个函数中被引用,那么从

    改变函数可能是最简单的
    myfunc <- function(...) {
      # do something
      COUNTS <- COUNTS + 1
    }
    

    myfunc <- local({
      COUNTS <- NA
      function(...) {
        # do something
        COUNTS <<- COUNTS + 1
      }
    })
    

    这样做是在myfunc“周围”创建一个本地环境,因此当它查找COUNTS 时,会立即找到。请注意,它使用 <<- 而不是 <- 重新分配,因为后者不会更新变量的不同环境版本。

    您实际上可以从包中的另一个函数访问此 COUNTS

    otherfunc <- function(...) {
      COUNTScopy <- get("COUNTS", envir = environment(myfunc))
      COUNTScopy <- COUNTScopy + 1
      assign("COUNTS", COUNTScopy, envir = environment(myfunc))
    }
    

    (这里也可以随意命名为COUNTS,我用了不同的名字来强调这无关紧要。)

    虽然使用 getassign 有点不方便,但每个需要执行此操作的函数只需要两次。

    请注意,如果需要,用户可以进行此操作,但他们需要使用类似的机制。也许这是个问题;在我需要某种形式的持久性的包中,我使用了方便的 getter/setter 函数。

  2. 您可以在您的包中放置一个环境,然后在您的包函数中像命名列表一样使用它:

    E <- new.env(parent = emptyenv())
    myfunc <- function(...) {
      # do something
      E$COUNTS <- E$COUNTS + 1
    }
    otherfunc <- function(...) {
      E$COUNTS <- E$COUNTS + 1
    }
    

    我们不需要 get/assign 这对函数,因为 E(一个可怕的名字,为简洁而选择)应该对包中的所有函数可见.如果您不需要用户具有访问权限,则不要将其导出。如果您希望用户能够访问它,那么通过正常的包机制导出它应该可行。

请注意,对于这两个,如果用户卸载并重新加载包,COUNTS 值将是 lost/reset。

我将列出提供第三个选项,以防用户 wants/needs 直接访问,或者您不想在您的包中进行此类价值管理。

  1. 让用户随时提供。为此,为每个需要它的函数添加一个参数,并让用户传递一个环境。我建议这样做,因为大多数参数都是按值传递的,但是环境允许引用语义(按引用传递)。

    例如,在您的包裹中:

    myfunc <- function(..., countenv) {
      stopifnot(is.environment(countenv))
      # do something
      countenv$COUNT <- countenv$COUNT + 1
    }
    otherfunc <- function(..., countenv) {
      countenv$COUNT <- countenv$COUNT + 1
    }
    new_countenv <- function(init = 0) {
      E <- new.env(parent = emptyenv())
      E$COUNT <- init
      E
    }
    

    其中 new_countenv 实际上只是一个方便的函数。

    用户随后会将您的包裹用作:

    mycount <- new_countenv()
    myfunc(..., countenv = mycount)
    otherfunc(..., countenv = mycount)