testing-r-packages
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTesting R Packages with testthat
使用testthat测试R包
Modern best practices for R package testing using testthat 3+.
使用testthat 3+进行R包测试的现代最佳实践。
Initial Setup
初始设置
Initialize testing with testthat 3rd edition:
r
usethis::use_testthat(3)This creates directory, adds testthat to Suggests with , and creates .
tests/testthat/DESCRIPTIONConfig/testthat/edition: 3tests/testthat.R使用testthat第3版初始化测试:
r
usethis::use_testthat(3)这会创建目录,将testthat添加到的Suggests字段并设置,同时创建文件。
tests/testthat/DESCRIPTIONConfig/testthat/edition: 3tests/testthat.RFile Organization
文件组织
Mirror package structure:
- Code in → tests in
R/foofy.Rtests/testthat/test-foofy.R - Use and
usethis::use_r("foofy")to create paired filesusethis::use_test("foofy")
Special files:
- - Helper functions and custom expectations, sourced before tests
helper-*.R - - Run during
setup-*.Ronly, not duringR CMD checkload_all() - - Static test data files accessed via
fixtures/test_path()
镜像包结构:
- 代码位于→ 测试位于
R/foofy.Rtests/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 Structure
测试结构
Tests follow a three-level hierarchy: File → Test → Expectation
测试遵循三级层级:文件 → 测试 → 断言
Standard Syntax
标准语法
r
test_that("descriptive behavior", {
result <- my_function(input)
expect_equal(result, expected_value)
})Test descriptions should read naturally and describe behavior, not implementation.
r
test_that("描述性行为", {
result <- my_function(input)
expect_equal(result, expected_value)
})测试描述应自然表述,描述行为而非实现细节。
BDD Syntax (describe/it)
BDD语法(describe/it)
For behavior-driven development, use and :
describe()it()r
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))
})
})Key features:
- groups related specifications for a component
describe() - defines individual specifications (like
it())test_that() - Supports nesting for hierarchical organization
- without code creates pending test placeholders
it()
Use to verify you implement the right things, use to ensure you do things right.
describe()test_that()See references/bdd.md for comprehensive BDD patterns, nested specifications, and test-first workflows.
对于行为驱动开发,使用和:
describe()it()r
describe("matrix()", {
it("可以与标量相乘", {
m1 <- matrix(1:4, 2, 2)
m2 <- m1 * 2
expect_equal(matrix(1:4 * 2, 2, 2), m2)
})
it("可以转置", {
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模式、嵌套规格和测试优先工作流。
Running Tests
运行测试
Three scales of testing:
Micro (interactive development):
r
devtools::load_all()
expect_equal(foofy(...), expected)Mezzo (single file):
r
testthat::test_file("tests/testthat/test-foofy.R")三种测试规模:
微测试(交互式开发):
r
devtools::load_all()
expect_equal(foofy(...), expected)中测试(单个文件):
r
testthat::test_file("tests/testthat/test-foofy.R")RStudio: Ctrl/Cmd + Shift + T
RStudio: Ctrl/Cmd + Shift + T
**Macro** (full suite):
```r
devtools::test() # Ctrl/Cmd + Shift + T
devtools::check() # Ctrl/Cmd + Shift + E
**宏测试**(完整套件):
```r
devtools::test() # Ctrl/Cmd + Shift + T
devtools::check() # Ctrl/Cmd + Shift + ECore Expectations
核心断言
Equality
相等性
r
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+)r
expect_equal(10, 10 + 1e-7) # 允许数值容差
expect_identical(10L, 10L) # 需要完全匹配
expect_all_equal(x, expected) # 每个元素都匹配(v3.3.0+)Errors, Warnings, Messages
错误、警告、消息
r
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())r
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())Pattern Matching
模式匹配
r
expect_match("Testing is fun!", "Testing")
expect_match(text, "pattern", ignore.case = TRUE)r
expect_match("Testing is fun!", "Testing")
expect_match(text, "pattern", ignore.case = TRUE)Structure and Type
结构与类型
r
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+r
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+Sets and Collections
集合
r
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+)r
expect_setequal(x, y) # 元素相同,顺序不限
expect_contains(fruits, "apple") # 子集检查(v3.2.0+)
expect_in("apple", fruits) # 元素是否在集合中(v3.2.0+)
expect_disjoint(set1, set2) # 无重叠(v3.3.0+)Logical
逻辑判断
r
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+)r
expect_true(condition)
expect_false(condition)
expect_all_true(vector > 0) # 所有元素为TRUE(v3.3.0+)
expect_all_false(vector < 0) # 所有元素为FALSE(v3.3.0+)Design Principles
设计原则
1. Self-Sufficient Tests
1. 自包含测试
Each test should contain all setup, execution, and teardown code:
r
undefined每个测试应包含所有设置、执行和清理代码:
r
undefinedGood: 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)
})
test_that("foofy() 正常工作", {
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)
})
undefineddat <- data.frame(x = 1:3, y = letters[1:3])
test_that("foofy() 正常工作", {
result <- foofy(dat) # 'dat' 来自哪里?
expect_equal(result$x, 1:3)
})
undefined2. Self-Contained Tests (Cleanup Side Effects)
2. 隔离测试(清理副作用)
Use to manage state changes:
withrr
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
})Common withr functions:
- - Temporarily set options
local_options() - - Temporarily set environment variables
local_envvar() - - Create temp file with automatic cleanup
local_tempfile() - - Create temp directory with automatic cleanup
local_tempdir() - - Temporarily attach package
local_package()
使用管理状态变更:
withrr
test_that("函数遵循选项设置", {
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")
# 测试后自动清理
})常用withr函数:
- - 临时设置选项
local_options() - - 临时设置环境变量
local_envvar() - - 创建临时文件并自动清理
local_tempfile() - - 创建临时目录并自动清理
local_tempdir() - - 临时加载包
local_package()
3. Plan for Test Failure
3. 为测试失败做准备
Write tests assuming they will fail and need debugging:
- Tests should run independently in fresh R sessions
- Avoid hidden dependencies on earlier tests
- Make test logic explicit and obvious
编写测试时假设它们会失败并需要调试:
- 测试应在全新的R会话中独立运行
- 避免依赖之前的测试
- 使测试逻辑清晰明确
4. Repetition is Acceptable
4. 重复是可接受的
Repeat setup code in tests rather than factoring it out. Test clarity is more important than avoiding duplication.
在测试中重复设置代码,而非将其提取出来。测试的清晰度比避免重复更重要。
5. Use devtools::load_all()
Workflow
devtools::load_all()5. 使用devtools::load_all()
工作流
devtools::load_all()During development:
- Use instead of
devtools::load_all()library() - Makes all functions available (including unexported)
- Automatically attaches testthat
- Eliminates need for calls in tests
library()
开发期间:
- 使用而非
devtools::load_all()library() - 可访问所有函数(包括未导出的)
- 自动加载testthat
- 无需在测试中调用
library()
Snapshot Testing
快照测试
For complex output that's difficult to verify programmatically, use snapshot tests. See references/snapshots.md for complete guide.
Basic pattern:
r
test_that("error message is helpful", {
expect_snapshot(
error = TRUE,
validate_input(NULL)
)
})Snapshots stored in .
tests/testthat/_snaps/Workflow:
r
devtools::test() # Creates new snapshots
testthat::snapshot_review('name') # Review changes
testthat::snapshot_accept('name') # Accept changes对于难以通过编程验证的复杂输出,使用快照测试。查看references/snapshots.md获取完整指南。
基本模式:
r
test_that("错误消息有帮助", {
expect_snapshot(
error = TRUE,
validate_input(NULL)
)
})快照存储在目录中。
tests/testthat/_snaps/工作流:
r
devtools::test() # 创建新快照
testthat::snapshot_review('name') # 查看变更
testthat::snapshot_accept('name') # 接受变更Test Fixtures and Data
测试固件与数据
Three approaches for test data:
1. Constructor functions - Create data on-demand:
r
new_sample_data <- function(n = 10) {
data.frame(id = seq_len(n), value = rnorm(n))
}2. Local functions with cleanup - Handle side effects:
r
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. Static fixture files - Store in directory:
fixtures/r
data <- readRDS(test_path("fixtures", "sample_data.rds"))See references/fixtures.md for detailed fixture patterns.
三种测试数据的处理方式:
1. 构造函数 - 按需创建数据:
r
new_sample_data <- function(n = 10) {
data.frame(id = seq_len(n), value = rnorm(n))
}2. 带清理的本地函数 - 处理副作用:
r
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/r
data <- readRDS(test_path("fixtures", "sample_data.rds"))查看references/fixtures.md获取详细的固件模式。
Mocking
模拟
Replace external dependencies during testing using . See references/mocking.md for comprehensive mocking strategies.
local_mocked_bindings()Basic pattern:
r
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")
})使用在测试期间替换外部依赖。查看references/mocking.md获取全面的模拟策略。
local_mocked_bindings()基本模式:
r
test_that("函数在模拟依赖下正常工作", {
local_mocked_bindings(
external_api = function(...) list(status = "success", data = "mocked")
)
result <- my_function_that_calls_api()
expect_equal(result$status, "success")
})Common Patterns
常见模式
Testing Errors with Specific Classes
测试特定类别的错误
r
test_that("validation catches errors", {
expect_error(
validate_input("wrong_type"),
class = "vctrs_error_cast"
)
})r
test_that("验证能捕获错误", {
expect_error(
validate_input("wrong_type"),
class = "vctrs_error_cast"
)
})Testing with Temporary Files
使用临时文件测试
r
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)
})r
test_that("文件处理正常", {
temp_file <- withr::local_tempfile(
lines = c("line1", "line2", "line3")
)
result <- process_file(temp_file)
expect_equal(length(result), 3)
})Testing with Modified Options
修改选项测试
r
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)
})r
test_that("输出遵循宽度设置", {
withr::local_options(width = 40)
output <- capture_output(print(my_object))
expect_lte(max(nchar(strsplit(output, "\n")[[1]])), 40)
})Testing Multiple Related Cases
测试多个相关案例
r
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")
})r
test_that("str_trunc() 处理所有方向", {
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")
})Custom Expectations in Helper Files
辅助文件中的自定义断言
r
undefinedr
undefinedIn tests/testthat/helper-expectations.R
在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, "@")
}
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("test@example.com")
expect_valid_user(user)
})
undefinedtest_that("用户创建正常", {
user <- create_user("test@example.com")
expect_valid_user(user)
})
undefinedFile System Discipline
文件系统规范
Always write to temp directory:
r
undefined始终写入临时目录:
r
undefinedGood
良好
output <- withr::local_tempfile(fileext = ".csv")
write.csv(data, output)
output <- withr::local_tempfile(fileext = ".csv")
write.csv(data, output)
Bad - writes to package directory
不良 - 写入包目录
write.csv(data, "output.csv")
**Access test fixtures with `test_path()`:**
```rwrite.csv(data, "output.csv")
**使用`test_path()`访问测试固件:**
```rGood - works in all contexts
良好 - 在所有环境中都能正常工作
data <- readRDS(test_path("fixtures", "data.rds"))
data <- readRDS(test_path("fixtures", "data.rds"))
Bad - relative paths break
不良 - 相对路径会失效
data <- readRDS("fixtures/data.rds")
undefineddata <- readRDS("fixtures/data.rds")
undefinedAdvanced Topics
高级主题
For advanced testing scenarios, see:
- references/bdd.md - BDD-style testing with describe/it, nested specifications, test-first workflows
- references/snapshots.md - Snapshot testing, transforms, variants
- references/mocking.md - Mocking strategies, webfakes, httptest2
- references/fixtures.md - Fixture patterns, database fixtures, helper files
- references/advanced.md - Skipping tests, secrets management, CRAN requirements, custom expectations, parallel testing
对于高级测试场景,查看:
- references/bdd.md - 使用describe/it的BDD风格测试、嵌套规格、测试优先工作流
- references/snapshots.md - 快照测试、转换、变体
- references/mocking.md - 模拟策略、webfakes、httptest2
- references/fixtures.md - 固件模式、数据库固件、辅助文件
- references/advanced.md - 跳过测试、密钥管理、CRAN要求、自定义断言、并行测试
testthat 3 Modernizations
testthat 3 现代化特性
When working with testthat 3 code, prefer modern patterns:
Deprecated → Modern:
- → Remove (duplicates filename)
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()
New in testthat 3:
- Edition system ()
Config/testthat/edition: 3 - Improved snapshot testing
- for better diff output
waldo::compare() - Unified condition handling
- works with byte-compiled code
local_mocked_bindings() - Parallel test execution support
使用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 新增特性:
- 版本系统()
Config/testthat/edition: 3 - 改进的快照测试
- 使用提供更优的差异输出
waldo::compare() - 统一的条件处理
- 支持字节编译代码
local_mocked_bindings() - 支持并行测试执行
Quick Reference
快速参考
Initialize:
usethis::use_testthat(3)Run tests: or Ctrl/Cmd + Shift + T
devtools::test()Create test file:
usethis::use_test("name")Review snapshots:
testthat::snapshot_review()Accept snapshots:
testthat::snapshot_accept()Find slow tests:
devtools::test(reporter = "slow")Shuffle tests:
devtools::test(shuffle = TRUE)初始化:
usethis::use_testthat(3)运行测试: 或 Ctrl/Cmd + Shift + T
devtools::test()创建测试文件:
usethis::use_test("name")查看快照:
testthat::snapshot_review()接受快照:
testthat::snapshot_accept()查找慢测试:
devtools::test(reporter = "slow")随机化测试顺序:
devtools::test(shuffle = TRUE)