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'” 的错误。
我错过了什么?我怎样才能正确编写这个测试(或设置我的包)。
我尝试解决的问题
- 在函数定义之前、内部或之后将
utils::globalVariables("COUNTS")
添加到 R/addx.R
。
- 在我能想到的所有地方将
utils::globalVariables("COUNTS")
添加到 tests/testthtat/test-addx.R
。
- 在我能想到的
tests/testthtat/test-addx.R
的所有地方手动初始化COUNTS
(例如,使用COUNTS <- 0
或<<- 0
)。
- 正在阅读 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
需要更新,所以把它放在包环境中可能是个问题。
您可以使用的一种技术是使用本地环境。
两种选择:
如果它总是在一个函数中被引用,那么从
改变函数可能是最简单的
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
,我用了不同的名字来强调这无关紧要。)
虽然使用 get
和 assign
有点不方便,但每个需要执行此操作的函数只需要两次。
请注意,如果需要,用户可以进行此操作,但他们需要使用类似的机制。也许这是个问题;在我需要某种形式的持久性的包中,我使用了方便的 getter/setter 函数。
您可以在您的包中放置一个环境,然后在您的包函数中像命名列表一样使用它:
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 直接访问,或者您不想在您的包中进行此类价值管理。
让用户随时提供。为此,为每个需要它的函数添加一个参数,并让用户传递一个环境。我建议这样做,因为大多数参数都是按值传递的,但是环境允许引用语义(按引用传递)。
例如,在您的包裹中:
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)
序言:包结构
我有一个 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'” 的错误。
我错过了什么?我怎样才能正确编写这个测试(或设置我的包)。
我尝试解决的问题
- 在函数定义之前、内部或之后将
utils::globalVariables("COUNTS")
添加到R/addx.R
。 - 在我能想到的所有地方将
utils::globalVariables("COUNTS")
添加到tests/testthtat/test-addx.R
。 - 在我能想到的
tests/testthtat/test-addx.R
的所有地方手动初始化COUNTS
(例如,使用COUNTS <- 0
或<<- 0
)。 - 正在阅读 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
需要更新,所以把它放在包环境中可能是个问题。
您可以使用的一种技术是使用本地环境。
两种选择:
如果它总是在一个函数中被引用,那么从
改变函数可能是最简单的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
,我用了不同的名字来强调这无关紧要。)虽然使用
get
和assign
有点不方便,但每个需要执行此操作的函数只需要两次。请注意,如果需要,用户可以进行此操作,但他们需要使用类似的机制。也许这是个问题;在我需要某种形式的持久性的包中,我使用了方便的 getter/setter 函数。
您可以在您的包中放置一个环境,然后在您的包函数中像命名列表一样使用它:
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 直接访问,或者您不想在您的包中进行此类价值管理。
让用户随时提供。为此,为每个需要它的函数添加一个参数,并让用户传递一个环境。我建议这样做,因为大多数参数都是按值传递的,但是环境允许引用语义(按引用传递)。
例如,在您的包裹中:
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)