Back to skills
SKILL.md
Write Testthat Tests
ASecurity为 R 包函数编写全面的 testthat(第 3 版)测试。涵盖测试组织、 断言、固件、模拟、快照测试、参数化测试及高覆盖率的实现。 适用于为新包函数添加测试、提升现有代码的测试覆盖率、为缺陷 修复编写回归测试,或为缺少测试基础设施的包进行搭建。
- 31 stars
- 0 votes
- 0 copies
- 1 view
- Added September 3, 2026
Works with
Security analysis
100/100npx -y skills add pjt222/agent-almanac --skill write-testthat-tests --agent claude-codeAre you the author of Write Testthat Tests?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/pjt222-write-testthat-tests-9cabb7fc)---
name: write-testthat-tests
description: >
为 R 包函数编写全面的 testthat(第 3 版)测试。涵盖测试组织、
断言、固件、模拟、快照测试、参数化测试及高覆盖率的实现。
适用于为新包函数添加测试、提升现有代码的测试覆盖率、为缺陷
修复编写回归测试,或为缺少测试基础设施的包进行搭建。
locale: zh-CN
source_locale: en
source_commit: 6f65f316
fence_basis_commit: 6f65f316
translator: claude-opus-4-6
translation_date: 2026-03-16
license: MIT
allowed-tools: Read Write Edit Bash Grep Glob
metadata:
author: Philipp Thoss
version: "1.0"
domain: r-packages
complexity: intermediate
language: R
tags: r, testthat, testing, unit-tests, coverage
---
# 编写 testthat 测试
使用 testthat 第 3 版为 R 包函数创建全面的测试。
## 适用场景
- 为新包函数添加测试
- 提升现有代码的测试覆盖率
- 为缺陷修复编写回归测试
- 为新包搭建测试基础设施
## 输入
- **必需**:需要测试的 R 函数
- **必需**:预期行为和边界情况
- **可选**:测试固件或示例数据
- **可选**:目标覆盖率百分比(默认:80%)
## 步骤
### 第 1 步:搭建测试基础设施
如尚未完成:
```r
usethis::use_testthat(edition = 3)
```
此步骤创建 `tests/testthat.R` 和 `tests/testthat/` 目录。
**预期结果:** `tests/testthat.R` 和 `tests/testthat/` 目录已创建。DESCRIPTION 中 `Config/testthat/edition: 3` 已设置。
**失败处理:** 若 usethis 不可用,手动创建 `tests/testthat.R`,内容为 `library(testthat); library(packagename); test_check("packagename")`,并创建 `tests/testthat/` 目录。
### 第 2 步:创建测试文件
```r
usethis::use_test("function_name")
```
此步骤在 `tests/testthat/test-function_name.R` 创建带有模板的测试文件。
**预期结果:** 测试文件在 `tests/testthat/test-function_name.R` 创建,包含待填写的 `test_that()` 占位块。
**失败处理:** 若 `usethis::use_test()` 不可用,手动创建文件,遵循 `test-<function_name>.R` 命名约定。
### 第 3 步:编写基础测试
```r
test_that("weighted_mean computes correct result", {
expect_equal(weighted_mean(1:3, c(1, 1, 1)), 2)
expect_equal(weighted_mean(c(10, 20), c(1, 3)), 17.5)
})
test_that("weighted_mean handles NA values", {
expect_equal(weighted_mean(c(1, NA, 3), c(1, 1, 1), na.rm = TRUE), 2)
expect_true(is.na(weighted_mean(c(1, NA, 3), c(1, 1, 1), na.rm = FALSE)))
})
test_that("weighted_mean validates input", {
expect_error(weighted_mean("a", 1), "numeric")
expect_error(weighted_mean(1:3, 1:2), "length")
})
```
**预期结果:** 基础测试涵盖典型输入的正确输出、NA 处理行为及输入验证错误信息。
**失败处理:** 若测试立即失败,确认函数已加载(`devtools::load_all()`)。若错误信息不匹配,在 `expect_error()` 中使用正则表达式模式而非精确字符串。
### 第 4 步:测试边界情况
```r
test_that("weighted_mean handles edge cases", {
# Empty input
expect_error(weighted_mean(numeric(0), numeric(0)))
# Single value
expect_equal(weighted_mean(5, 1), 5)
# Zero weights
expect_true(is.nan(weighted_mean(1:3, c(0, 0, 0))))
# Very large values
expect_equal(weighted_mean(c(1e15, 1e15), c(1, 1)), 1e15)
# Negative weights
expect_error(weighted_mean(1:3, c(-1, 1, 1)))
})
```
**预期结果:** 边界情况已覆盖:空输入、单一值、零权重、极端值及无效输入。每个边界情况都有明确的预期行为。
**失败处理:** 若函数对某边界情况的处理与预期不符,决定是修复函数还是调整测试。对于存在歧义的情况,记录预期行为。
### 第 5 步:为复杂测试使用固件
创建 `tests/testthat/fixtures/` 存放测试数据:
```r
# tests/testthat/helper.R (loaded automatically)
create_test_data <- function() {
data.frame(
x = c(1, 2, 3, NA, 5),
group = c("a", "a", "b", "b", "b")
)
}
```
```r
# In test file
test_that("process_data works with grouped data", {
test_data <- create_test_data()
result <- process_data(test_data)
expect_s3_class(result, "data.frame")
expect_equal(nrow(result), 2)
})
```
**预期结果:** 固件为多个测试文件提供一致的测试数据。`tests/testthat/helper.R` 中的辅助函数由 testthat 自动加载。
**失败处理:** 若找不到辅助函数,确认文件名为 `helper.R`(而非 `helpers.R`)且位于 `tests/testthat/`。必要时重启 R 会话。
### 第 6 步:模拟外部依赖
```r
test_that("fetch_data handles API errors", {
local_mocked_bindings(
api_call = function(...) stop("Connection refused")
)
expect_error(fetch_data("endpoint"), "Connection refused")
})
test_that("fetch_data returns parsed data", {
local_mocked_bindings(
api_call = function(...) list(data = list(value = 42))
)
result <- fetch_data("endpoint")
expect_equal(result$value, 42)
})
```
**预期结果:** 外部依赖(API、数据库、网络调用)已模拟,测试无需真实连接即可运行。模拟返回值用于检验函数的数据处理逻辑。
**失败处理:** 若 `local_mocked_bindings()` 失败,确认被模拟的函数在测试作用域内可访问。对于其他包中的函数,使用 `.package` 参数。
### 第 7 步:对复杂输出使用快照测试
```r
test_that("format_report produces expected output", {
expect_snapshot(format_report(test_data))
})
test_that("plot_results creates expected plot", {
expect_snapshot_file(
save_plot(plot_results(test_data), "test-plot.png"),
"expected-plot.png"
)
})
```
**预期结果:** 快照文件创建于 `tests/testthat/_snaps/`。首次运行创建基准;后续运行与之对比。
**失败处理:** 若快照在有意更改后失败,使用 `testthat::snapshot_accept()` 更新它们。对于跨平台差异,使用 `variant` 参数维护平台特定的快照。
### 第 8 步:使用跳过条件
```r
test_that("database query works", {
skip_on_cran()
skip_if_not(has_db_connection(), "No database available")
result <- query_db("SELECT 1")
expect_equal(result[[1]], 1)
})
test_that("parallel computation works", {
skip_on_os("windows")
skip_if(parallel::detectCores() < 2, "Need multiple cores")
result <- parallel_compute(1:100)
expect_length(result, 100)
})
```
**预期结果:** 需要特殊环境(网络、数据库、多核)的测试已用跳过条件保护。这些测试在本地运行,但在 CRAN 或受限 CI 环境中跳过。
**失败处理:** 若测试在 CRAN 或 CI 上失败但本地通过,在 `test_that()` 块顶部添加适当的 `skip_on_cran()`、`skip_on_os()` 或 `skip_if_not()` 保护。
### 第 9 步:运行测试并检查覆盖率
```r
# Run all tests
devtools::test()
# Run specific test file
devtools::test_active_file() # in RStudio
testthat::test_file("tests/testthat/test-function_name.R")
# Check coverage
covr::package_coverage()
covr::report()
```
**预期结果:** 所有测试通过 `devtools::test()`。覆盖率报告显示达到目标百分比(目标 >80%)。
**失败处理:** 若测试失败,查看测试输出中的具体断言失败信息。若覆盖率低于目标,使用 `covr::report()` 识别未测试的代码路径并为其补充测试。
## 验证清单
- [ ] 所有测试通过 `devtools::test()`
- [ ] 覆盖率超过目标百分比
- [ ] 每个导出函数至少有一个测试
- [ ] 错误条件已测试
- [ ] 边界情况已覆盖(NA、NULL、空值、边界值)
- [ ] 无测试依赖外部状态或执行顺序
## 常见问题
- **测试相互依赖**:每个 `test_that()` 块必须相互独立
- **硬编码文件路径**:对测试固件使用 `testthat::test_path()`
- **浮点数比较**:使用 `expect_equal()`(含容差)而非 `expect_identical()`
- **测试私有函数**:尽量通过公开 API 测试,谨慎使用 `:::`
- **CI 中的快照测试**:快照对平台敏感,跨平台时使用 `variant` 参数
- **忘记 `skip_on_cran()`**:需要网络、数据库或长时间运行的测试必须在 CRAN 上跳过
## 示例
```r
# Pattern: test file mirrors R/ file
# R/weighted_mean.R -> tests/testthat/test-weighted_mean.R
# Pattern: descriptive test names
test_that("weighted_mean returns NA when na.rm = FALSE and input contains NA", {
result <- weighted_mean(c(1, NA), c(1, 1), na.rm = FALSE)
expect_true(is.na(result))
})
# Pattern: testing warnings
test_that("deprecated_function emits deprecation warning", {
expect_warning(deprecated_function(), "deprecated")
})
```
## 相关技能
- `create-r-package` — 在创建包时搭建测试基础设施
- `write-roxygen-docs` — 为被测函数编写文档
- `setup-github-actions-ci` — 在推送时自动运行测试
- `submit-to-cran` — CRAN 要求测试在所有平台上通过
Attribution
Comments
Loading comments…