mcpField
T
testing-r-packages

testing-r-packages

github.com/posit-dev测试与质量551749 次安装今天更新
代码仓库

关于 testing-r-packages

面向 R 包开发者的 testthat 3 测试实践指南,涵盖测试组织、断言、快照、模拟与夹具,支持从单个测试到完整测试套件的工作流。

主要功能

  1. 01testthat 3 初始化与文件组织
  2. 02BDD 测试与断言用法
  3. 03快照测试与审查流程
  4. 04夹具、模拟及副作用清理
  5. 05微型至完整套件的测试运行

适用场景

  1. 01为 R 包建立测试目录和规范
  2. 02验证函数结果、错误与警告
  3. 03隔离外部依赖并管理临时文件

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/posit-dev/skills --skill testing-r-packages

安装后在 Claude Code 中输入/testing-r-packages确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
advanced.md
bdd.md
fixtures.md
mocking.md
snapshots.md

详细介绍

使用 testthat 测试 R 包

使用 testthat 3+ 测试 R 包的现代最佳实践。

初始设置

使用 testthat 第 3 版初始化测试:

usethis::use_testthat(3)

此操作会创建 tests/testthat/ 目录,将 testthat 添加到 DESCRIPTION 的 Suggests 中,并设置 Config/testthat/edition: 3,同时创建 tests/testthat.R。

文件组织

镜像包结构:

  • R/foofy.R 中的代码 → tests/testthat/test-foofy.R 中的测试
  • 使用 usethis::use_r("foofy") 和 usethis::use_test("foofy") 创建配对文件

特殊文件:

  • helper-*.R - 辅助函数和自定义断言,会在测试前载入
  • setup-*.R - 仅在 R CMD check 期间运行,不会在 load_all() 期间运行
  • fixtures/ - 静态测试数据文件,可通过 test_path() 访问

测试结构

测试遵循三级层次结构:文件 → 测试 → 断言

标准语法
test_that("descriptive behavior", {
  result <- my_function(input)
  expect_equal(result, expected_value)
})

测试描述应读起来通顺,并描述行为,而非实现方式。

BDD 语法(describe/it)

如需采用行为驱动开发,请使用 describe() 和 it():

describe("matrix()", {
  it("can be multiplied by a scalar", {
    m1 <- matrix(1:4, 2, 2)
    m2 <- m1 * 2
    expect_equal(matrix(1:4 * 2, 2, 2), m2)
  })

  it("can be transposed", {
    m <- matrix(1:4, 2, 2)
    expect_equal(t(m), matrix(c(1, 3, 2, 4), 2, 2))
  })
})

主要特性:

  • describe() 将相关规格分组到同一组件下
  • it() 定义单个规格(类似 test_that())
  • 支持嵌套,以实现层次化组织
  • 不包含代码的 it() 会创建待编写测试的占位项

使用 describe() 验证你实现了正确的内容,使用 test_that() 确保你正确地完成了工作。

请参阅 references/bdd.md,了解完整的 BDD 模式、嵌套规格和测试优先工作流。

运行测试

测试分为三个规模:

微型(交互式开发):

devtools::load_all()
expect_equal(foofy(...), expected)

中型(单个文件):

testthat::test_file("tests/testthat/test-foofy.R")
# RStudio: Ctrl/Cmd + Shift + T

大型(完整测试套件):

devtools::test()    # Ctrl/Cmd + Shift + T
devtools::check()   # Ctrl/Cmd + Shift + E

核心断言

相等性
expect_equal(10, 10 + 1e-7)      # Allows numeric tolerance
expect_identical(10L, 10L)       # Exact match required
expect_all_equal(x, expected)    # Every element matches (v3.3.0+)
错误、警告、消息
expect_error(1 / "a")
expect_error(bad_call(), class = "specific_error_class")
expect_no_error(valid_call())

expect_warning(deprecated_func())
expect_no_warning(safe_func())

expect_message(informative_func())
expect_no_message(quiet_func())
模式匹配
expect_match("Testing is fun!", "Testing")
expect_match(text, "pattern", ignore.case = TRUE)
结构与类型
expect_length(vector, 10)
expect_type(obj, "list")
expect_s3_class(model, "lm")
expect_s4_class(obj, "MyS4Class")
expect_r6_class(obj, "MyR6Class")      # v3.3.0+
expect_shape(matrix, c(10, 5))         # v3.3.0+
集合与容器
expect_setequal(x, y)           # Same elements, any order
expect_contains(fruits, "apple") # Subset check (v3.2.0+)
expect_in("apple", fruits)       # Element in set (v3.2.0+)
expect_disjoint(set1, set2)      # No overlap (v3.3.0+)
逻辑
expect_true(condition)
expect_false(condition)
expect_all_true(vector > 0)      # All elements TRUE (v3.3.0+)
expect_all_false(vector < 0)     # All elements FALSE (v3.3.0+)

设计原则

1. 自足的测试

每个测试都应包含完整的设置、执行和清理代码:

# Good: self-contained
test_that("foofy() works", {
  data <- data.frame(x = 1:3, y = letters[1:3])
  result <- foofy(data)
  expect_equal(result$x, 1:3)
})

# Bad: relies on ambient state
dat <- data.frame(x = 1:3, y = letters[1:3])
test_that("foofy() works", {
  result <- foofy(dat)  # Where did 'dat' come from?
  expect_equal(result$x, 1:3)
})
2. 自包含的测试(清理副作用)

使用 withr 管理状态变更:

test_that("function respects options", {
  withr::local_options(my_option = "test_value")
  withr::local_envvar(MY_VAR = "test")
  withr::local_package("jsonlite")

  result <- my_function()
  expect_equal(result$setting, "test_value")
  # Automatic cleanup after test
})

常用的 withr 函数:

  • local_options() - 临时设置选项
  • local_envvar() - 临时设置环境变量
  • local_tempfile() - 创建临时文件并自动清理
  • local_tempdir() - 创建临时目录并自动清理
  • local_package() - 临时附加包
3. 为测试失败做好准备

编写测试时应假设测试会失败,并且需要调试:

  • 测试应能在全新的 R 会话中独立运行
  • 避免依赖之前测试的隐性依赖
  • 让测试逻辑清晰明了
4. 重复是可以接受的

与其提取代码,不如在测试中重复设置代码。测试清晰度比避免重复更重要。

5. 使用 devtools::load_all() 工作流

开发期间:

  • 使用 devtools::load_all(),而不是 library()
  • 使所有函数都可用(包括未导出的函数)
  • 自动附加 testthat
  • 无需在测试中调用 library()

快照测试

对于难以通过程序验证的复杂输出,请使用快照测试。请参阅 references/snapshots.md 了解完整指南。

基本模式:

test_that("error message is helpful", {
  expect_snapshot(
    error = TRUE,
    validate_input(NULL)
  )
})

快照存储在 tests/testthat/_snaps/ 中。

工作流:

devtools::test()                    # Creates new snapshots
testthat::snapshot_review('name')   # Review changes
testthat::snapshot_accept('name')   # Accept changes

测试夹具与数据

测试数据有三种处理方式:

1. 构造函数 - 按需创建数据:

new_sample_data <- function(n = 10) {
  data.frame(id = seq_len(n), value = rnorm(n))
}

2. 带清理操作的局部函数 - 处理副作用:

local_temp_csv <- function(data, env = parent.frame()) {
  path <- withr::local_tempfile(fileext = ".csv", .local_envir = env)
  write.csv(data, path, row.names = FALSE)
  path
}

3. 静态夹具文件 - 存储在 fixtures/ 目录中:

data <- readRDS(test_path("fixtures", "sample_data.rds"))

请参阅 references/fixtures.md,了解详细的夹具模式。

模拟

使用 local_mocked_bindings() 在测试期间替换外部依赖。请参阅 references/mocking.md,了解全面的模拟策略。

基本模式:

test_that("function works with mocked dependency", {
  local_mocked_bindings(
    external_api = function(...) list(status = "success", data = "mocked")
  )

  result <- my_function_that_calls_api()
  expect_equal(result$status, "success")
})

常见模式

使用特定类测试错误
test_that("validation catches errors", {
  expect_error(
    validate_input("wrong_type"),
    class = "vctrs_error_cast"
  )
})
使用临时文件进行测试
test_that("file processing works", {
  temp_file <- withr::local_tempfile(
    lines = c("line1", "line2", "line3")
  )

  result <- process_file(temp_file)
  expect_equal(length(result), 3)
})
使用修改后的选项进行测试
test_that("output respects width", {
  withr::local_options(width = 40)

  output <- capture_output(print(my_object))
  expect_lte(max(nchar(strsplit(output, "\n")[[1]])), 40)
})
测试多个相关用例
test_that("str_trunc() handles all directions", {
  trunc <- function(direction) {
    str_trunc("This string is moderately long", direction, width = 20)
  }

  expect_equal(trunc("right"), "This string is mo...")
  expect_equal(trunc("left"), "...erately long")
  expect_equal(trunc("center"), "This stri...ely long")
})
在辅助文件中定义自定义断言
# In tests/testthat/helper-expectations.R
expect_valid_user <- function(user) {
  expect_type(user, "list")
  expect_named(user, c("id", "name", "email"))
  expect_type(user$id, "integer")
  expect_match(user$email, "@")
}

# In test file
test_that("user creation works", {
  user <- create_user("[email protected]")
  expect_valid_user(user)
})

文件系统规范

始终写入临时目录:

# Good
output <- withr::local_tempfile(fileext = ".csv")
write.csv(data, output)

# Bad - writes to package directory
write.csv(data, "output.csv")

使用 test_path() 访问测试夹具:

# Good - works in all contexts
data <- readRDS(test_path("fixtures", "data.rds"))

# Bad - relative paths break
data <- readRDS("fixtures/data.rds")

高级主题

有关高级测试场景,请参阅:

testthat 3 的现代化改进

处理 testthat 3 代码时,优先使用现代模式:

已弃用 → 现代写法:

  • context() → 移除(与文件名重复)
  • expect_equivalent() → expect_equal(ignore_attr = TRUE)
  • with_mock() → local_mocked_bindings()
  • is_null()、is_true()、is_false() → expect_null()、expect_true()、expect_false()

testthat 3 中的新功能:

  • Edition 系统(Config/testthat/edition: 3)
  • 改进的快照测试
  • 使用 waldo::compare() 获得更好的差异输出
  • 统一的条件处理
  • local_mocked_bindings() 可用于字节编译代码
  • 支持并行执行测试

快速参考

初始化: usethis::use_testthat(3)

运行测试: devtools::test() 或 Ctrl/Cmd + Shift + T

创建测试文件: usethis::use_test("name")

查看快照: testthat::snapshot_review()

接受快照: testthat::snapshot_accept()

查找耗时较长的测试: devtools::test(reporter = "slow")

随机打乱测试: devtools::test(shuffle = TRUE)

评论

支持 Markdown