Usage¶
pytest-r-snapshot records reference outputs from labelled R code chunks embedded in your Python test files, then compares your Python outputs to those recorded snapshots.
Basic fixture usage¶
Use the r_snapshot fixture and compare text output:
def test_summary_matches_r(r_snapshot):
# ```{r, summary}
# x <- c(1, 2, 3)
# summary(x)
# ```
actual = my_python_summary(...)
r_snapshot.assert_match_text(actual, name="summary")
Record snapshots locally:
By default, CI can run pytest in replay mode without R.
Embedding R chunks¶
Commented chunks¶
Commented chunks are convenient when you want the R code to live right above the assertion:
def test_x(r_snapshot):
# ```{r, label}
# x <- 1 + 1
# print(x)
# ```
r_snapshot.assert_match_text("2", name="label")
Rules:
- Start/end fences must be a standalone line containing three backticks.
- Each body line must start with
#(an optional single space after#is allowed). - Every chunk must have a label, and labels must be unique per file.
Docstring / multiline string chunks¶
You can also place raw fenced chunks inside a docstring or multiline string:
def test_summary_matches_r(r_snapshot):
"""
```{r, summary}
x <- c(1, 2, 3)
summary(x)
```
"""
r_snapshot.assert_match_text(my_python_summary(...), name="summary")
The plugin dedents the chunk body to remove Python indentation while keeping relative indentation.
Snapshot file layout¶
By default, for tests/test_example.py, snapshots are stored under:
tests/__r_snapshots__/test_example/<name><ext>
You can override the snapshot root directory with --r-snapshot-dir / r_snapshot_dir. In that case snapshots are stored under:
<r_snapshot_dir>/test_example/<name><ext>
The default file extension is .txt. You can change it per assertion:
Both "rtf" and ".rtf" are accepted.
Snapshot modes¶
replay(default): never runs R; fails if the snapshot is missing.record: always runs R and overwrites snapshots.auto: runs R only when a snapshot is missing.
Examples:
R execution configuration (when recording)¶
These options matter when the plugin needs to run R (record mode, or auto with a missing snapshot):
--r-snapshot-rscript=PATH/r_snapshot_rscript: whichRscriptto run--r-snapshot-cwd=PATH/r_snapshot_cwd: working directory for R--r-snapshot-env=KEY=VALUE/r_snapshot_env: environment overrides (repeatable)--r-snapshot-timeout=SECONDS/r_snapshot_timeout: per-chunk timeout--r-snapshot-encoding=ENC/r_snapshot_encoding: snapshot file encoding
See docs/articles/configuration.md for the full reference and precedence rules.
Markers (optional)¶
You can declare snapshot dependencies at the test boundary:
import pytest
@pytest.mark.r_snapshot("summary")
def test_summary_matches_r(r_snapshot):
r_snapshot.assert_match_text(my_python_summary(...), name="summary")
Or use the decorator alias:
from pytest_r_snapshot import r_snapshot
@r_snapshot("summary")
def test_summary_matches_r(r_snapshot):
...
The marker is repeatable. It is used for better errors (for example, a test declares snapshot X but no chunk X exists).
Normalizing output¶
The normalize= hook lets you apply domain-specific normalization to both expected and actual text before comparison.
Example: normalize newlines and strip trailing whitespace:
from pytest_r_snapshot import normalize_newlines, strip_trailing_whitespace
def normalize(text: str) -> str:
return strip_trailing_whitespace(normalize_newlines(text))
def test_output(r_snapshot):
...
r_snapshot.assert_match_text(actual, name="out", normalize=normalize)
Reading and recording explicitly¶
r_snapshot.read_text(name="...")reads a snapshot file (fails if missing).r_snapshot.record_text(name="...")runs the labelled R chunk and writes the snapshot file.r_snapshot.path_for(name="...")returns the snapshot path.
Providing R code explicitly¶
For advanced use cases, you can pass R code directly via code=... to record_text() or assert_match_text():
- If
codecontains fenced chunks, the chunk with labelnameis used. - Otherwise
codeis treated as the raw R body to execute.