# Tdd Workflow

> Test-driven development workflow for R using testthat. Use when writing new features, fixing bugs, or refactoring code. Enforces test-first development with 80%+ coverage.

- **Type:** Skill
- **Install:** `agentstack add skill-ab604-claude-code-r-skills-tdd-workflow`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [ab604](https://agentstack.voostack.com/s/ab604)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [ab604](https://github.com/ab604)
- **Source:** https://github.com/ab604/claude-code-r-skills/tree/main/.claude/skills/tdd-workflow

## Install

```sh
agentstack add skill-ab604-claude-code-r-skills-tdd-workflow
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Test-Driven Development Workflow for R

This skill ensures all R code development follows TDD principles with comprehensive test coverage using testthat.

## When to Activate

- Writing new functions or features
- Fixing bugs or issues
- Refactoring existing code
- Adding new model types
- Creating data processing pipelines
- Building Shiny components

## Getting Started

Initialize testing infrastructure for your package:

```r
# 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")
```

## Core Principles

### 1. Tests BEFORE Code

ALWAYS write tests first, then implement code to make tests pass.

### 2. Coverage Requirements

- Minimum 80% coverage (unit + integration)
- 100% coverage for statistical calculations
- 100% coverage for data validation
- All edge cases covered
- Error scenarios tested

### 3. Test Types

Tests follow a three-level hierarchy: **File → Test → Expectation**

#### Unit Tests

Individual functions and utilities:

```r
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))
})
```

#### Integration Tests

Function interactions and workflows:

```r
test_that("data pipeline produces expected output", {
  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))
})
```

#### Snapshot Tests

For complex outputs that are hard to specify:

```r
test_that("model summary format is stable", {
  model  ci_95["upper"] - ci_95["lower"])
})

test_that("calculate_ci handles NA values", {
  set.seed(123)
  result = 1) {
    cli::cli_abort("{.arg conf_level} must be between 0 and 1", class = "validation_error")
  }

  # Remove NA values
  x = 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))
  )
}
```

### Step 7: Verify Coverage

```r
covr::package_coverage()
# calculate_ci.R: 100%
```

## Testing Patterns

### Testing Data Transformations

```r
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))
})
```

### Testing Statistical Functions

```r
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)
})
```

### Testing with Fixtures

```r
# 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)
})
```

### Mocking External Dependencies

```r
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"
  )
})
```

### Using withr for Cleanup

Use `withr` functions to manage temporary state with automatic restoration:

```r
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")
})
```

## Test Data Strategies

Choose the appropriate approach for your testing needs:

### 1. Constructor Functions

Create data on-demand with helper functions:

```r
# 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. Local Functions with Cleanup

Handle side effects using withr:

```r
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. Static Fixtures

Store data files in `fixtures/` directory:

```r
# 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))
})
```

## Common Testing Mistakes to Avoid

### WRONG: Testing Implementation Details

```r
# Don't test internal state
expect_equal(obj$internal_cache, expected_cache)
```

### CORRECT: Test Behavior

```r
# Test observable behavior
expect_equal(get_result(obj), expected_result)
```

### WRONG: Brittle Tests

```r
# Breaks on any output change
expect_equal(as.character(result), "Mean: 5.234567890")
```

### CORRECT: Flexible Assertions

```r
# Robust to formatting changes
expect_equal(result$mean, 5.23, tolerance = 0.01)
```

### WRONG: Dependent Tests

```r
test_that("creates data", { global_data <<- create() })
test_that("uses data", { process(global_data) })  # Depends on previous!
```

### CORRECT: Independent Tests

```r
test_that("creates and uses data", {
  data <- create()
  result <- process(data)
  expect_true(is_valid(result))
})
```

### WRONG: Modifying Tests to Pass

```r
# 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!
})
```

### CORRECT: Fix the Implementation

```r
# Fix the code to match expected behavior
test_that("function returns 42", {
  expect_equal(my_function(), 42)  # Test fails
})

# Fix my_function() implementation instead
```

## When Tests Fail

1. **Do NOT modify tests** to make them pass (unless the test is wrong)
2. **Fix the implementation** to match expected behavior
3. **Add more tests** if the failure reveals missing coverage
4. **Update snapshots** only if the change is intentional

```r
# Review and accept snapshot changes
testthat::snapshot_review("test_name")
testthat::snapshot_accept("test_name")
```

## Coverage Verification

```r
# 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
)
```

## Debugging & Development

### Running Tests at Different Scales

```r
# 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
```

### Test Reporters

```r
# Find slow tests
devtools::test(reporter = "slow")

# Progress reporter (verbose)
devtools::test(reporter = "progress")

# Test execution order independence
devtools::test(shuffle = TRUE)
```

### Continuous Testing

```r
# Watch mode - auto-run on file changes
testthat::auto_test_package()
```

### Parallel Execution (Edition 3)

Edition 3 supports parallel test execution for faster runs on multi-core systems.

## Running Tests

```r
# 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()
```

## Success Metrics

- 80%+ code coverage achieved
- All tests passing
- No skipped tests
- Fast execution (< 30s for unit tests)
- Tests catch bugs before production
- Confident refactoring enabled
- Tests run independently in any order
- Clear, descriptive test names
- Each test validates one concept

---

**Remember**: Tests are not optional. They are the safety net that enables confident refactoring, rapid development, and production reliability. Write them FIRST.

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [ab604](https://github.com/ab604)
- **Source:** [ab604/claude-code-r-skills](https://github.com/ab604/claude-code-r-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-ab604-claude-code-r-skills-tdd-workflow
- Seller: https://agentstack.voostack.com/s/ab604
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
