R包测试中如何正确修改package命名空间内的变量
我开发了一个R包,在包加载阶段会向package命名空间赋值一个变量,实现代码如下:
.onLoad <- function(libname, pkgname){ assign(".myvar", "foo", envir = parent.env(environment())) }
在包内部,我通过如下方式调用该变量:
myfn <- function(){ print(.myvar) }
当前在单元测试环节,我需要在运行测试用例前修改该变量的值。
test_that("myfn works correctly", { myvar_tmp <- .myvar withr::defer(assign(".myvar", myvar_tmp , pos = "package:mypackage")) assign(".myvar", "bar", pos = "package:mypackage") expect_equal(myfn(), "bar") })
但运行该测试时出现如下失败提示:
── Failure (Line 6): myfn works correctly ────────────────────────────────────── myfn() not equal to "bar". 1/1 mismatches x[1]: "foo" y[1]: "bar"
显然我并未正确更新.myvar的值,myfn仍然读取的是包加载时设置的初始值。
当我在测试外部尝试设置该变量时,
> assign(".myvar", "bar", pos = "package:mypackage") > .myvar # [1] "bar"
通过检查确认该变量确实属于目标包的命名空间,并非其他环境下的同名变量:
pryr::where(".myvar") <environment: package:mypackage> attr(,"name") [1] "package:mypackage" attr(,"path") [1] "/home/user/Documents/mypackage"
但即便如此,调用函数时
> myfn() #[1] "foo"
函数并未使用新赋的值,仍然返回包加载时设置的初始值。
问题: 该场景下正确更新package命名空间中变量的方式是什么?
注:以上为演示核心原理的简化示例,实际使用场景更复杂。我知晓测试场景下修改该变量的必要性并不直观,但实际业务中确实存在该需求。
之前的赋值操作修改的是搜索路径上的package:mypackage导出环境,不是包的私有命名空间环境。R包内部函数查找变量时,会优先检索自身的私有命名空间,只有命名空间内找不到对应变量时,才会逐层往导出环境等上级环境查找。你在.onLoad中已经把.myvar赋值到了包的私有命名空间,所以包内函数始终会读取命名空间内的初始值,不会感知到你在导出环境中新建的同名变量。
直接将值赋值到包的真实命名空间环境即可,不要用pos = "package:mypackage"指定导出环境,通过asNamespace()获取包的私有命名空间作为赋值目标:
test_that("myfn works correctly", { # 获取包的私有命名空间环境 ns <- asNamespace("mypackage") # 备份变量原始值 myvar_tmp <- get(".myvar", envir = ns) # 测试结束后自动恢复原始值,避免影响其他用例 withr::defer(assign(".myvar", myvar_tmp, envir = ns)) # 直接修改命名空间内的变量值 assign(".myvar", "bar", envir = ns) expect_equal(myfn(), "bar") })
交互式环境下临时修改逻辑完全一致:
assign(".myvar", "bar", envir = asNamespace("mypackage")) myfn() # 此时会正确输出"bar"
注意:这种直接修改包命名空间内部变量的方式仅适用于单元测试等临时场景,正式业务代码中不建议使用,会破坏包的封装性,容易引入不可预期的副作用。
内容的提问来源于stack exchange,提问作者Dan

