tdd-workflow について
testthat を使う R 開発者向けのテスト駆動開発スキルです。実装前にテストを書く方針に基づき、テスト構成やカバレッジ確認の手順を示します。Edition 3 による単体・統合・スナップショットテストも扱います。
主な機能
- 01実装前にテストを作成
- 02単体・統合・スナップショットテストの構成
- 03testthat Edition 3 の利用
- 04独立した振る舞い中心のテスト
- 05カバレッジ確認とテストデータ整理
活用シーン
- 01新しい関数や機能をテスト
- 02バグ修正時に期待する動作を検証
- 03リファクタリング中もテストを維持
- 04データ処理パイプラインや Shiny コンポーネントをテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ ab604/ claude-code-r-skills --skill tdd-workflowインストール後、Claude Code で/tdd-workflowを入力して読み込まれたか確認します。
詳細
R のテスト駆動開発ワークフロー
このスキルは、testthat を使って包括的なテストカバレッジを確保し、すべての R コード開発で TDD の原則に従えるようにします。
有効にするタイミング
- 新しい関数や機能を作成するとき
- バグや問題を修正するとき
- 既存のコードをリファクタリングするとき
- 新しいモデルタイプを追加するとき
- データ処理パイプラインを作成するとき
- Shiny コンポーネントを構築するとき
はじめに
パッケージのテスト基盤を初期化します。
# Set up testthat (Edition 3)
usethis::use_testthat(3)
# Create a test file for an existing source file
usethis::use_test("function_name")
# Or create test and source file together
usethis::use_r("function_name")
usethis::use_test("function_name")
基本原則
1. コードを書く前にテストを書く
必ず最初にテストを書き、その後、テストに合格するようにコードを実装します。
2. カバレッジ要件
- 最低 80% のカバレッジ(単体テスト+統合テスト)
- 統計計算は 100% のカバレッジ
- データ検証は 100% のカバレッジ
- すべてのエッジケースを網羅
- エラーが発生するケースをテスト
3. テストの種類
テストは 3 段階の階層に従います:ファイル → テスト → 期待値
単体テスト
個々の関数とユーティリティ:
test_that("rescale01 normalizes to [0, 1] range", {
expect_equal(rescale01(c(0, 5, 10)), c(0, 0.5, 1))
expect_equal(rescale01(c(-10, 0, 10)), c(0, 0.5, 1))
})
test_that("rescale01 handles edge cases", {
expect_equal(rescale01(c(5, 5, 5)), c(NaN, NaN, NaN))
expect_equal(rescale01(numeric(0)), numeric(0))
expect_equal(rescale01(c(0, NA, 10)), c(0, NA, 1))
})
統合テスト
関数間の連携とワークフロー:
test_that("data pipeline produces expected output", {
raw_data <- read_fixture("sample_input.csv")
result <- raw_data |>
clean_data() |>
transform_features() |>
summarize_results()
expect_s3_class(result, "tbl_df")
expect_named(result, c("group", "mean", "sd", "n"))
expect_true(all(result$n > 0))
})
スナップショットテスト
仕様を定義しにくい複雑な出力に使用します。
test_that("model summary format is stable", {
model <- fit_model(test_data)
expect_snapshot(print(summary(model)))
})
test_that("error messages are informative", {
expect_snapshot(
validate_input(invalid_data),
error = TRUE
)
})
スナップショットのワークフロー:
# Review snapshot changes
testthat::snapshot_review("test_name")
# Accept snapshot changes
testthat::snapshot_accept("test_name")
スナップショットは tests/testthat/_snaps/ ディレクトリに保存されます。
BDD の代替方法(任意)
振る舞い駆動開発では、describe() と it() を使用します。
describe("matrix()", {
it("can be multiplied by a scalar", {
m1 <- matrix(1:4, 2, 2)
m2 <- m1 * 2
expect_equal(matrix(c(2, 4, 6, 8), 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() は適切なものを実装しているかを確認し、test_that() は正しく実装しているかを確認します。」
テスト設計の原則
テストの自己完結性
各テストには、セットアップ、実行、後処理のコードをすべて含めます。テストは互いに独立し、周囲の状態や先行するテストの実行結果に依存せず、単独で実行できる必要があります。
# GOOD: Self-contained
test_that("function works with specific data", {
data <- tibble(x = 1:10, y = rnorm(10)) # Setup
result <- my_function(data) # Execute
expect_equal(nrow(result), 10) # Assert
})
# BAD: Depends on external state
# setup_data <- tibble(...) # Created outside test
test_that("function works", {
result <- my_function(setup_data) # Relies on external data
expect_equal(nrow(result), 10)
})
関数化より重複を優先
テストでは繰り返しが許容されます。セットアップコードは別の場所に切り出さず、重複させてください。重複を避けることよりも、明確さを優先します。
# GOOD: Duplicated but clear
test_that("clean_data handles missing values", {
data <- tibble(x = c(1, NA, 3), y = c(4, 5, 6))
result <- clean_data(data)
expect_equal(nrow(result), 2)
})
test_that("clean_data handles invalid values", {
data <- tibble(x = c(1, -999, 3), y = c(4, 5, 6))
result <- clean_data(data, invalid = -999)
expect_equal(nrow(result), 2)
})
# ACCEPTABLE: Each test is self-contained and readable
失敗を前提に計画する
テストは失敗し、デバッグが必要になることを前提に記述します。ロジックを明示的かつ明快にします。新しい R セッションでテストを個別に実行します。
devtools::load_all() を使用する
開発中は library() よりも devtools::load_all() を使用します。これにより、次のことが可能になります。
- エクスポートされていない関数をテストで利用できる
- testthat が自動的にアタッチされる
- テスト内で不要な
library()呼び出しをなくせる - インストールせずにパッケージの読み込みをシミュレートできる
testthat エディション 3
エディション 3 では、スナップショットテストの改善、waldo による差分表示の向上、条件処理の統一、並列実行のサポート、モックでのバイトコンパイル済みコードとの互換性が提供されます。
非推奨のパターン → 最新の代替方法
# DEPRECATED: context() calls
context("Data validation") # Remove - filename serves this purpose
# DEPRECATED: expect_equivalent()
expect_equivalent(x, y)
# MODERN:
expect_equal(x, y, ignore_attr = TRUE)
# DEPRECATED: with_mock()
with_mock(external_call = function() "mocked", {
result <- my_function()
})
# MODERN:
local_mocked_bindings(
external_call = function() "mocked"
)
result <- my_function()
# DEPRECATED: expect_is()
expect_is(x, "data.frame")
# MODERN:
expect_s3_class(x, "data.frame")
エディション 3 を初期化する
DESCRIPTION で次を確認します。
Config/testthat/edition: 3
または、次のように初期化します。
usethis::use_testthat(3)
期待値に関する必須リファレンス
等価性と同一性
expect_equal(x, y) # With numeric tolerance
expect_equal(x, y, tolerance = 0.001)
expect_equal(x, y, ignore_attr = TRUE)
expect_identical(x, y) # Exact match required
expect_all_equal(x) # Every element equal (v3.3.0+)
条件
expect_error(code)
expect_error(code, "pattern")
expect_error(code, class = "validation_error")
expect_warning(code)
expect_no_warning(code)
expect_message(code)
expect_no_message(code)
コレクションと集合
expect_setequal(x, y) # Same elements, any order
expect_contains(set, element) # Subset relationship (v3.2.0+)
expect_in(element, set) # Membership check (v3.2.0+)
expect_disjoint(set1, set2) # No overlap (v3.3.0+)
expect_named(x, c("a", "b")) # Named vector/list
型と構造
expect_type(x, "double")
expect_s3_class(x, "data.frame")
expect_s4_class(x, "S4Class")
expect_r6_class(x, "R6Class")
expect_shape(matrix, c(2, 3)) # Matrix/array dimensions (v3.3.0+)
expect_length(x, 10)
論理値
expect_true(x)
expect_false(x)
expect_all_true(x) # Every element TRUE (v3.3.0+)
expect_all_false(x) # Every element FALSE (v3.3.0+)
その他の便利な期待値
expect_null(x)
expect_invisible(result)
expect_output(print(x), "pattern")
expect_snapshot(complex_output)
ファイル構成
テストはパッケージの構造に対応させます。
tests/
├── testthat/
│ ├── test-validation.R # Tests for R/validation.R
│ ├── test-processing.R # Tests for R/processing.R
│ ├── test-models.R # Tests for R/models.R
│ ├── test-output.R # Tests for R/output.R
│ ├── helper-fixtures.R # Shared functions (sourced before tests)
│ ├── setup-database.R # Setup code (runs during R CMD check)
│ ├── helper-expectations.R # Custom expectations
│ └── fixtures/ # Static test data files
│ ├── sample_input.csv
│ └── expected_output.rds
└── testthat.R # Test runner
ファイルの種類
test-*.R- 実際のテストファイル(ソースファイルと対になります)helper-*.R- テスト実行前に読み込まれる共有ユーティリティ関数setup-*.R-R CMD checkの実行中にのみ実行されるセットアップコードfixtures/-test_path("fixtures/file")でアクセスする静的なテストデータ
フィクスチャにアクセスします。
test_path("fixtures", "sample_data.csv")
TDD ワークフローの手順
手順 1:期待される動作を定義する
関数がどのように動作すべきかを記述します。
# Function: calculate_ci
# Purpose: Calculate bootstrap confidence intervals
# Inputs:
# - data: numeric vector
# - conf_level: confidence level (default 0.95)
# - n_boot: number of bootstrap samples (default 1000)
# Outputs:
# - Named numeric vector with lower and upper bounds
# Edge cases:
# - Handle NA values
# - Error on non-numeric input
# - Error on empty input
手順 2:失敗するテストを書く
# tests/testthat/test-calculate_ci.R
library(testthat)
test_that("calculate_ci returns correct structure", {
set.seed(123)
result <- calculate_ci(1:100)
expect_type(result, "double")
expect_named(result, c("lower", "upper"))
expect_true(result["lower"] < result["upper"])
})
test_that("calculate_ci respects confidence level", {
set.seed(123)
ci_95 <- calculate_ci(1:100, conf_level = 0.95)
ci_99 <- calculate_ci(1:100, conf_level = 0.99)
# 99% CI should be wider
expect_true(ci_99["upper"] - ci_99["lower"] > ci_95["upper"] - ci_95["lower"])
})
test_that("calculate_ci handles NA values", {
set.seed(123)
result <- calculate_ci(c(1:100, NA, NA))
expect_false(any(is.na(result)))
})
test_that("calculate_ci validates inputs", {
expect_error(calculate_ci("not numeric"), class = "validation_error")
expect_error(calculate_ci(numeric(0)), class = "validation_error")
expect_error(calculate_ci(1:10, conf_level = 1.5), class = "validation_error")
})
手順 3:テストを実行する(失敗するはずです)
devtools::test()
# ✖ calculate_ci returns correct structure
# ✖ calculate_ci respects confidence level
# ✖ calculate_ci handles NA values
# ✖ calculate_ci validates inputs
手順 4:最小限のコードを実装する
# R/calculate_ci.R
#' Calculate Bootstrap Confidence Interval
#'
#' @param x Numeric vector
#' @param conf_level Confidence level (default 0.95)
#' @param n_boot Number of bootstrap samples (default 1000)
#' @return Named numeric vector with lower and upper bounds
#' @export
calculate_ci <- function(x, conf_level = 0.95, n_boot = 1000) {
# Validate inputs
if (!is.numeric(x)) {
cli::cli_abort("{.arg x} must be numeric", class = "validation_error")
}
if (length(x) == 0) {
cli::cli_abort("{.arg x} cannot be empty", class = "validation_error")
}
if (conf_level <= 0 || conf_level >= 1) {
cli::cli_abort("{.arg conf_level} must be between 0 and 1", class = "validation_error")
}
# Remove NA values
x <- x[!is.na(x)]
# Bootstrap
boot_means <- replicate(n_boot, mean(sample(x, replace = TRUE)))
# Calculate quantiles
alpha <- 1 - conf_level
c(
lower = unname(quantile(boot_means, alpha / 2)),
upper = unname(quantile(boot_means, 1 - alpha / 2))
)
}
手順 5:テストを再実行する
devtools::test()
# ✔ calculate_ci returns correct structure
# ✔ calculate_ci respects confidence level
# ✔ calculate_ci handles NA values
# ✔ calculate_ci validates inputs
手順 6:リファクタリングする
テストがすべて成功する状態を保ちながら改善します。
# Extract validation to helper
validate_ci_inputs <- function(x, conf_level) {
if (!is.numeric(x)) {
cli::cli_abort("{.arg x} must be numeric", class = "validation_error")
}
if (length(x) == 0) {
cli::cli_abort("{.arg x} cannot be empty", class = "validation_error")
}
if (conf_level <= 0 || conf_level >= 1) {
cli::cli_abort("{.arg conf_level} must be between 0 and 1", class = "validation_error")
}
}
calculate_ci <- function(x, conf_level = 0.95, n_boot = 1000) {
validate_ci_inputs(x, conf_level)
x <- x[!is.na(x)]
boot_means <- replicate(n_boot, mean(sample(x, replace = TRUE)))
alpha <- 1 - conf_level
c(
lower = unname(quantile(boot_means, alpha / 2)),
upper = unname(quantile(boot_means, 1 - alpha / 2))
)
}
手順 7:カバレッジを確認する
covr::package_coverage()
# calculate_ci.R: 100%
テストのパターン
データ変換のテスト
test_that("clean_data removes invalid rows", {
input <- tibble(
id = 1:4,
value = c(1, NA, 3, -999)
)
result <- clean_data(input, invalid_value = -999)
expect_equal(nrow(result), 2)
expect_equal(result$id, c(1, 3))
expect_false(anyNA(result$value))
})
統計関数のテスト
test_that("weighted_mean matches manual calculation", {
x <- c(1, 2, 3)
w <- c(1, 2, 1)
result <- weighted_mean(x, w)
expected <- sum(x * w) / sum(w) # (1 + 4 + 3) / 4 = 2
expect_equal(result, expected)
})
フィクスチャを使ったテスト
# helper-fixtures.R
read_fixture <- function(name) {
path <- testthat::test_path("fixtures", name)
readr::read_csv(path, show_col_types = FALSE)
}
# test-pipeline.R
test_that("pipeline handles real data", {
input <- read_fixture("sample_data.csv")
result <- process_pipeline(input)
expect_snapshot(result)
})
外部依存関係のモック
test_that("fetch_data handles API errors", {
# Mock the API call
local_mocked_bindings(
httr2_request = function(...) {
stop("API unavailable")
}
)
expect_error(
fetch_data("endpoint"),
"API unavailable"
)
})
クリーンアップに withr を使用する
withr の関数を使って一時的な状態を管理し、自動的に元に戻します。
test_that("function respects options", {
# Temporarily set options
withr::local_options(list(digits = 2))
result <- format_number(3.14159)
expect_equal(result, "3.14")
})
test_that("function writes to temp file", {
# Create temp file that's automatically cleaned up
tmp <- withr::local_tempfile(lines = c("line 1", "line 2"))
result <- process_file(tmp)
expect_equal(result$n_lines, 2)
})
test_that("function uses custom environment variable", {
# Temporarily set env var
withr::local_envvar(MY_VAR = "test_value")
result <- get_config()
expect_equal(result$my_var, "test_value")
})
テストデータ戦略
テストのニーズに応じて、適切な方法を選択します。
1. コンストラクター関数
ヘルパー関数を使って、必要に応じてデータを作成します。
# helper-data.R
make_sample_data <- function(n = 100) {
tibble(
id = 1:n,
group = sample(c("A", "B"), n, replace = TRUE),
value = rnorm(n)
)
}
# test-analysis.R
test_that("analysis handles grouped data", {
data <- make_sample_data(n = 50)
result <- analyze_groups(data)
expect_s3_class(result, "tbl_df")
})
2. クリーンアップを伴うローカル関数
withr を使って副作用を処理します。
test_that("function reads CSV correctly", {
# Create temp file with cleanup
tmp <- withr::local_tempfile(fileext = ".csv")
write.csv(mtcars, tmp, row.names = FALSE)
result <- read_and_process(tmp)
expect_equal(nrow(result), 32)
})
3. 静的フィクスチャ
データファイルを fixtures/ ディレクトリに保存します。
# Store in: tests/testthat/fixtures/sample_data.csv
test_that("function handles real data format", {
path <- test_path("fixtures", "sample_data.csv")
data <- read_csv(path)
result <- process_data(data)
expect_true(all(result$valid))
})
避けるべきよくあるテストの間違い
誤り:実装の詳細をテストする
# Don't test internal state
expect_equal(obj$internal_cache, expected_cache)
正しい方法:動作をテストする
# Test observable behavior
expect_equal(get_result(obj), expected_result)
誤り:壊れやすいテスト
# Breaks on any output change
expect_equal(as.character(result), "Mean: 5.234567890")
正しい方法:柔軟なアサーション
# Robust to formatting changes
expect_equal(result$mean, 5.23, tolerance = 0.01)
誤り:依存関係のあるテスト
test_that("creates data", { global_data <<- create() })
test_that("uses data", { process(global_data) }) # Depends on previous!
正しい方法:独立したテスト
test_that("creates and uses data", {
data <- create()
result <- process(data)
expect_true(is_valid(result))
})
誤り:テストを通すためにテストを変更する
# When a test fails, don't change the test (unless it's wrong)
test_that("function returns 42", {
expect_equal(my_function(), 42) # Test fails
})
# DON'T DO THIS:
test_that("function returns 41", {
expect_equal(my_function(), 41) # Changed to pass - WRONG!
})
正しい方法:実装を修正する
# Fix the code to match expected behavior
test_that("function returns 42", {
expect_equal(my_function(), 42) # Test fails
})
# Fix my_function() implementation instead
テストが失敗した場合
- テストが間違っている場合を除き、テストを通すためにテストを変更しないでください
- 期待される動作に合うように、実装を修正します
- 失敗によってカバレッジの不足が明らかになった場合は、テストを追加します
- 変更が意図したものである場合に限り、スナップショットを更新します
# Review and accept snapshot changes
testthat::snapshot_review("test_name")
testthat::snapshot_accept("test_name")
カバレッジの確認
# Run coverage report
covr::package_coverage()
# Interactive HTML report
covr::report()
# Check specific thresholds
cov <- covr::package_coverage()
pct <- covr::percent_coverage(cov)
if (pct < 80) {
stop("Coverage below 80%: ", round(pct, 1), "%")
}
# In testthat.R or as a coverage check
covr::package_coverage(
type = "all",
line_coverage = 0.80,
function_coverage = 0.80
)
デバッグと開発
規模を変えてテストを実行する
# Micro: Interactive development
devtools::load_all()
expect_equal(my_function(1), 1) # Direct expectation
# Mezzo: Single file
testthat::test_file("tests/testthat/test-validation.R")
# RStudio: Ctrl/Cmd+Shift+T
# Macro: Full suite
devtools::test()
devtools::check() # Full package validation
テストレポーター
# Find slow tests
devtools::test(reporter = "slow")
# Progress reporter (verbose)
devtools::test(reporter = "progress")
# Test execution order independence
devtools::test(shuffle = TRUE)
継続的なテスト
# Watch mode - auto-run on file changes
testthat::auto_test_package()
並列実行(Edition 3)
Edition 3 は、マルチコアシステムでテストをより速く実行できるよう、並列テスト実行をサポートしています。
テストの実行
# All tests
devtools::test()
# All tests (keyboard shortcut)
# RStudio: Ctrl/Cmd+Shift+T
# With coverage
covr::package_coverage()
# Specific file
testthat::test_file("tests/testthat/test-validation.R")
# Watch mode
testthat::auto_test_package()
# Verbose output
devtools::test(reporter = "progress")
# Find slow tests
devtools::test(reporter = "slow")
# Test independence
devtools::test(shuffle = TRUE)
# Full package check
devtools::check()
成功指標
- コードカバレッジ 80% 以上を達成
- すべてのテストが成功する
- スキップされたテストがない
- 実行が速い(ユニットテストで 30 秒未満)
- テストによって本番環境に出る前にバグを検出できる
- 自信を持ってリファクタリングできる
- テストを任意の順序で個別に実行できる
- 明確で説明的なテスト名を付ける
- 各テストで 1 つの概念を検証する
覚えておいてください:テストは省略できません。テストは、自信を持ったリファクタリング、迅速な開発、本番環境での信頼性を実現するセーフティネットです。テストは最初に書いてください。