npx claudepluginhub pjt222/agent-almanacThis skill uses the workspace's default tool permissions.
---
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.
Guides web app testing strategy: test pyramid (unit/integration/component/E2E), framework selection (Vitest/Playwright/Jest), coverage rules, mocking boundaries, and execution.
Share bugs, ideas, or general feedback.
Set up comprehensive testing for Shiny applications using shinytest2 (end-to-end) and testServer() (unit tests).
install.packages("shinytest2")
# For golem apps, add as a Suggests dependency
usethis::use_package("shinytest2", type = "Suggests")
# Set up testthat infrastructure if not present
usethis::use_testthat(edition = 3)
Expected: shinytest2 installed and testthat directory structure in place.
On failure: shinytest2 requires chromote (headless Chrome). Install Chrome/Chromium on the system. On WSL: sudo apt install -y chromium-browser. Verify with chromote::find_chrome().
Create tests/testthat/test-mod_dashboard.R:
test_that("dashboard module filters data correctly", {
testServer(dataFilterServer, args = list(
data = reactive(iris),
columns = c("Species", "Sepal.Length")
), {
# Set inputs
session$setInputs(column = "Species")
session$setInputs(value_select = "setosa")
session$setInputs(apply = 1)
# Check output
result <- filtered()
expect_equal(nrow(result), 50)
expect_true(all(result$Species == "setosa"))
})
})
test_that("dashboard module handles empty data", {
testServer(dataFilterServer, args = list(
data = reactive(iris[0, ]),
columns = c("Species")
), {
# Module should not error on empty data
expect_no_error(session$setInputs(column = "Species"))
})
})
Key patterns:
testServer() tests module server logic without a browserargs listsession$setInputs() to simulate user interactionsExpected: Module tests pass with devtools::test().
On failure: If testServer() errors with "not a module server function", ensure the function uses moduleServer() internally. If session$setInputs() doesn't trigger reactives, add session$flushReact() after setting inputs.
Create tests/testthat/test-app-e2e.R:
test_that("app loads and displays initial state", {
# For golem apps
app <- AppDriver$new(
app_dir = system.file(package = "myapp"),
name = "initial-load",
height = 800,
width = 1200
)
on.exit(app$stop(), add = TRUE)
# Wait for app to load
app$wait_for_idle(timeout = 10000)
# Check that key elements exist
app$expect_values()
})
test_that("filter interaction updates the table", {
app <- AppDriver$new(
app_dir = system.file(package = "myapp"),
name = "filter-interaction"
)
on.exit(app$stop(), add = TRUE)
# Interact with the app
app$set_inputs(`filter1-column` = "cyl")
app$wait_for_idle()
app$set_inputs(`filter1-apply` = "click")
app$wait_for_idle()
# Snapshot the output values
app$expect_values(output = "table")
})
Key patterns:
AppDriver$new() launches the app in headless Chromeon.exit(app$stop()) to clean up"moduleId-inputId"app$expect_values() creates/compares snapshot filesapp$wait_for_idle() ensures reactive updates completeExpected: End-to-end tests create snapshot files in tests/testthat/_snaps/.
On failure: If Chrome isn't found, set CHROMOTE_CHROME environment variable to the Chrome binary path. If snapshots fail on CI but pass locally, check for platform-dependent rendering differences — use app$expect_values() for data snapshots rather than app$expect_screenshot() for visual ones.
shinytest2::record_test("path/to/app")
This opens the app in a browser with a recording panel. Interact with the app, then click "Save test" to auto-generate test code.
Expected: A test file is generated in tests/testthat/ with recorded interactions.
On failure: If the recorder doesn't open, check that the app runs successfully with shiny::runApp() first. The recorder requires a working app.
For snapshot-based tests, manage expected values:
# Accept new/changed snapshots after review
testthat::snapshot_accept("test-app-e2e")
# Review snapshot differences
testthat::snapshot_review("test-app-e2e")
Add snapshot directories to version control:
tests/testthat/_snaps/ # Committed — contains expected values
Expected: Snapshot files tracked in git for regression detection.
On failure: If snapshots change unexpectedly, run testthat::snapshot_review() to see the diffs. Accept intentional changes with testthat::snapshot_accept().
Add to .github/workflows/R-CMD-check.yaml or create a dedicated workflow:
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y chromium-browser
- name: Set Chrome path
run: echo "CHROMOTE_CHROME=$(which chromium-browser)" >> $GITHUB_ENV
- name: Run tests
run: |
Rscript -e 'devtools::test()'
For golem apps, ensure the app package is installed before testing:
- name: Install app package
run: Rscript -e 'devtools::install()'
Expected: Tests pass in CI with headless Chrome.
On failure: Common CI issues: Chrome not installed (add the apt-get step), display server missing (shinytest2 uses headless mode by default so this usually isn't an issue), or timeout on slow runners (increase timeout in AppDriver$new()).
devtools::test() runs all tests without errorstestServer() for logic and app$expect_values() for data. Only use app$expect_screenshot() when visual appearance matters — screenshots are brittle across platforms."moduleId-inputId" format (hyphen-separated), not "moduleId.inputId".app$wait_for_idle() after app$set_inputs(). Without it, assertions may run before reactive updates complete.build-shiny-module — create testable modules with clear interfacesscaffold-shiny-app — set up app structure with testing infrastructurewrite-testthat-tests — general testthat patterns for R packagessetup-github-actions-ci — CI/CD setup for R packages (golem apps)