【问题标题】:How can we test functions that aren't exposed when building R packages?我们如何测试构建 R 包时未公开的函数?
【发布时间】:2011-05-18 07:08:44
【问题描述】:

我目前正在为 R 开发一个 graphical analysis package。我们正在尝试使用来自 Clean CodeTest-Driven Development (TDD) 的原则。但是,我们遇到了一个概念问题。

如何测试未公开的函数?

考虑以下简化示例。 Outer() 是我们在package 中构建的一个函数,我们通过在NAMESPACE 文件中列出它来向用户公开它。 Inner() 是一个简短的(~5 行)实用函数:

Outer <- function(...) {

  Inner <- function(...) {
    return(x)
  }

  return( Inner() )
}

package 中的大多数用户公开函数都是短的、单一行为的 Inner() 样式函数的集合,它们位于用户可以调用的单个 Outer() 函数的保护伞下。 Granova.ds.ggplot() 就是一个例子。 Clean Code 强烈提倡这样的模块化设计,我们对结果非常满意。但是我们不知道如何为它构建单元测试,因为我们想要测试的函数在我们设计的Granova.ds.ggplot() 函数的范围之外是无法访问的。

我们想到了几个想法/解决方案:

  1. 测试应该只能访问公共 API。由于像 Inner() 这样的函数在设计上是不公开的(它们不会在 NAMESPACE 中导出),我们甚至不应该尝试测试它们。
  2. Hadley Wickham 的 testthat package wiki 表示它支持使用 R CMD check 工作流测试“非导出函数”。
  3. 如果一切都失败了,我们可以以某种方式手动破坏我们的 Outer() 函数以进行测试

到目前为止,这些解决方案都没有奏效

解决方案 1 似乎是一种逃避。我们相信,特别是在 R 中,拥有一个调用各种简短、单一职责实用程序方法的顶级函数是明智的。无法测试这样的方法似乎很愚蠢,就像有解决方案但我们还没有找到的东西类型。

解决方案 2 可能有效,但到目前为止我们还没有让它发挥作用。如果您尝试克隆our repository,然后采购inst/dev.R,我相信您会在测试输出中发现它找不到函数InitializeGgplot(),即使said functiongranova.1w.ggplot() 的第613-615 行中定义。同样,在终端运行R CMD check 似乎根本不会执行我们的任何测试。不过,这确实需要大量时间并向我们抛出侮辱性错误:-(

解决方案 3 在某种意义上是务实的,但与始终朝着项目的目标状态前进的目标背道而驰。这对我们来说没有意义。

理想的解决方案是什么

理想情况下,我们正在寻找一种方法来利用像 testthat 这样的包在我们编码时快速提供反馈,并允许我们测试像 Inner() 这样存在于函数中的函数喜欢Outer()。更好的方法是不用R CMD check,因为我们的一些函数的 3-d 复杂性,每次运行几乎需要整整一分钟。

那么,我们缺少什么? TDD 实践是否应该允许在 R 中测试外部/内部样式设置?如果他们这样做,我们如何在开发我们的包时这样做?欢迎任何反馈,我会尽力回复任何不清楚的地方。

谢谢!

【问题讨论】:

  • 如果你的 Inner 函数实际上只定义在 within Outer 的代码中,我相信这些只是在调用 Outer 时才实际创建的。所以我看不到测试这些的方法。对于其他非嵌套但非导出的函数,应该可以通过 getAnywhere 等进行测试。
  • 您能否解释一下选项 3 中“项目的目标状态”的含义?我会将这些内部函数分解出来,但不会在 API 中公开它们(即不将函数导出到 NAMESPACE)。因此,您可以记录和测试它们,并且仍然拥有干净的 API。
  • @Andrie - 好问题;我不清楚。我们的解决方案之一涉及您建议的伪版本。我们会故意破坏Outer()(比如通过注释掉它的函数声明)来强制Inner()函数是顶级的。稍后,我们将取消注释掉这些行。对我来说,这感觉就像远离目标状态,因为这意味着在现在故意破坏暴露的函数,并且必须记住在我们构建之前稍后再次解除它。

标签: unit-testing r tdd


【解决方案1】:

如果Inner 实现了您想要测试的重要功能,我建议将Inner 移动到顶层,但不要导出它。通常,正是出于这个原因,我避免在其他函数中嵌套函数——它们很难测试。

您可以在开发期间使用通常的 testthat 函数进行测试,因为您可能只是在所有 R 代码中进行采购,而不用担心命名空间(至少我是这样开发的)。然后,您可以将 R CMD checktest_package 结合使用,以确保测试在构建时仍然有效 - test_packages 在包命名空间中运行测试,以便它们可以测试非导出的函数。

【讨论】:

  • 您的一般建议是更像模型 1:cl.ly/0D1k341b1o3J3S3A3o32 还是像模型 2:cl.ly/2Y3g3p1045002B1K3X18?此外,对于任一模型,我需要在构建脚本或 /tests 文件中编写/包含什么以确保测试代码可以执行 Inner() 函数?
  • 好的,我想我刚刚想通了。在顶层将相似的函数聚合到一个文件中可能是最简单的。然后,我可以有一个脚本调用library(testthat),获取包含函数的文件,并为每个测试文件调用test_file()。对不起!
  • 是的,在 github 上查看我的 devtools 包,了解自动化该过程的功能
  • @hadley 你有这个我可以看的简单例子吗?会很方便。
【解决方案2】:

IMO 这里没有问题——Inner 只是 Outer 不可拆卸的一部分,所以测试 Outer 测试 Inner。你愿意测试匿名函数吗?这里也一样。

【讨论】:

  • 在我的情况下,非导出函数是不平凡的。移动部分比典型的匿名函数多,因此测试很有用。
猜你喜欢
  • 1970-01-01
  • 2016-04-21
  • 2021-09-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-07-04
  • 1970-01-01
相关资源
最近更新 更多