TestSheet — pytest for Excel
Capture a golden-master baseline from your Excel model, then automatically flag any cell whose value or formula drifts in future versions.
TestSheet is a command-line regression-testing tool for spreadsheet models. It works the same way pytest works for code: you record what should be true, then run the suite on every change to verify nothing broke.
Features
- Golden-master baseline — snapshot every cell's value and formula in one command
- Drift detection — six drift kinds:
value_only,formula_only,both,new,deleted,error_introduced - Invariant rules — six built-in rule types you describe in a YAML file (range bounds, error checks, totals tie-outs, monotonic sequences, cross-cell relationships, no-hardcode guards)
- Multiple reporters — rich console table, JSON, JUnit XML (for CI), standalone HTML
- Structural-change heuristic — warns when too many cells drift at once (likely a layout change, not real model drift)
- Configurable float tolerance — set
rel_tol/abs_tolin a config file - GitHub Actions integration — single composite action, reports uploaded as artifacts
Installation
pip install testsheet
Dependencies: Python 3.10+, openpyxl, typer, rich, pyyaml, jinja2, formulas
5-minute quickstart
1. Capture a baseline
Navigate to the folder containing your workbook and run:
testsheet baseline models/q4_forecast.xlsx
This creates .testsheet/baseline.json next to your workbook. Commit this file to version control.
models/
q4_forecast.xlsx
.testsheet/
baseline.json ← commit this
2. Run the checks
After any edit to the workbook:
testsheet run models/q4_forecast.xlsx
TestSheet prints a table of drifted cells and exits 0 (pass) or 1 (fail).
TestSheet — running checks on q4_forecast.xlsx
┌──────────┬─────────┬───────────────────┬───────────┬───────────┐
│ Sheet │ Address │ Kind │ Baseline │ Current │
├──────────┼─────────┼───────────────────┼───────────┼───────────┤
│ Summary │ B2 │ value_only │ 460.0 │ 999.0 │
│ Model │ D2 │ error_introduced │ 120 │ #DIV/0! │
└──────────┴─────────┴───────────────────┴───────────┴───────────┘
FAIL — 2 cells drifted
3. Add invariant rules (optional)
Create .testsheet/rules.yaml:
rules:
- id: revenues_positive
type: range_bound
range: "Model!A1:A12"
min: 0
- id: no_formula_errors
type: no_error
range: "Model!A1:Z200"
- id: total_ties_sum
type: totals_tie
total_cell: "Summary!B10"
sum_range: "Summary!B1:B9"
tolerance: 0.01
- id: margins_increasing
type: monotonic
range: "Model!C1:C4"
direction: increasing
- id: no_hardcoded_drivers
type: no_hardcode_in_range
range: "Assumptions!B1:B20"
- id: gross_profit_nonneg
type: relationship
expression: "Summary!B5 >= 0"
Rules are evaluated on every testsheet run. A rule failure counts the same as cell drift for the exit code.
CLI reference
testsheet baseline <workbook>
Captures a golden-master baseline.
| Option | Default | Description |
|---|---|---|
--sheets |
all | Comma-separated sheet names to include |
--evaluator |
formulas |
formulas or libreoffice |
testsheet run <workbook>
Runs regression checks against the baseline.
| Option | Default | Description |
|---|---|---|
--evaluator |
formulas |
formulas or libreoffice |
--junit PATH |
— | Write JUnit XML report |
--json PATH |
— | Write JSON report |
--html PATH |
— | Write standalone HTML report |
--no-fail-on-drift |
— | Exit 0 even when drift is found |
Rules reference
| Type | Required keys | Description |
|---|---|---|
range_bound |
range or cell, optionally min, max |
Every cell in range must be within [min, max] |
no_error |
range |
No Excel error string (#REF!, #DIV/0!, etc.) in range |
no_hardcode_in_range |
range |
Every cell in range must contain a formula, not a literal |
totals_tie |
total_cell, sum_range, tolerance |
total_cell ≈ SUM(sum_range) within tolerance |
monotonic |
range, direction (increasing|decreasing), optionally strict |
Values in range must be monotonically ordered |
relationship |
expression |
Arbitrary cross-cell expression evaluates to True (e.g. "Summary!B5 >= Summary!B4 * 0.9") |
Configuration
Create .testsheet/config.yaml to override defaults:
diff:
rel_tol: 1.0e-6 # relative float tolerance (default 1e-9)
abs_tol: 1.0e-9 # absolute float tolerance (default 1e-12)
structural_change_threshold: 0.5 # warn when this fraction of cells drift (default 0.5)
Float tolerance — useful for models with rounding differences between Excel versions. Set rel_tol: 0.01 to ignore differences smaller than 1%.
Structural change threshold — when the fraction of drifted cells exceeds this value, TestSheet emits a warning that the workbook layout may have changed (rows/columns inserted, sheet renamed), rather than flagging hundreds of individual cell drifts.
GitHub Actions
Add to your workflow:
- name: Run TestSheet
uses: ./action.yml # or the published action path
with:
workbook: models/q4_forecast.xlsx
junit-output: testsheet-report.xml
html-output: testsheet-report.html
- name: Publish test results
uses: dorny/test-reporter@v1
if: always()
with:
name: TestSheet
path: testsheet-report.xml
reporter: java-junit
Full example workflow (.github/workflows/testsheet.yml):
name: Excel regression gate
on:
pull_request:
paths:
- "models/**"
jobs:
testsheet:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run TestSheet
uses: ./
with:
workbook: models/q4_forecast.xlsx
- name: Publish JUnit results
uses: dorny/test-reporter@v1
if: always()
with:
name: TestSheet
path: testsheet-report.xml
reporter: java-junit
How it works
-
baseline— loads the workbook twice (once withdata_only=Falsefor formulas, once withdata_only=Truefor cached values), then runs theformulaspure-Python evaluator to recompute values from scratch. Writes.testsheet/baseline.json. -
run— repeats the same parse + evaluate step on the current workbook, then diffs cell-by-cell against the baseline. Classifies each changed cell into one of six drift kinds. Evaluates anyrules.yamlinvariants. Exits non-zero if any drift or rule failure is found. -
Evaluator — TestSheet uses the
formulaspure-Python Excel evaluator (no LibreOffice required). A LibreOffice headless evaluator is also available via--evaluator libreofficefor models that use functions not yet supported byformulas.
Project layout
testsheet/
├── src/testsheet/
│ ├── cli.py # Typer CLI (baseline + run)
│ ├── parser.py # openpyxl workbook parser
│ ├── baseline.py # golden-master capture + load
│ ├── diff.py # drift detection + structural-change heuristic
│ ├── config.py # .testsheet/config.yaml loader
│ ├── evaluator/
│ │ ├── formulas_eval.py # formulas library evaluator (default)
│ │ ├── pycel_eval.py # pycel evaluator (legacy / Python ≤3.11)
│ │ └── libreoffice.py # LibreOffice headless evaluator
│ ├── rules/
│ │ └── engine.py # rules.yaml loader + 6 rule handlers
│ └── reporters/
│ ├── console.py # rich terminal table
│ ├── json_reporter.py
│ ├── junit.py # JUnit XML
│ └── html_reporter.py # standalone HTML
├── tests/
│ ├── test_smoke.py
│ ├── test_evaluators.py
│ ├── test_diff_m2.py
│ ├── test_rules_m3.py
│ ├── test_rules_e2e.py
│ ├── test_reporters_m4.py
│ ├── test_ci_m4.py
│ └── test_hardening_m5.py
├── action.yml # GitHub Actions composite action
└── pyproject.toml
License
MIT
Metadata
Release files for testsheet-xl 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| testsheet_xl-0.1.0.tar.gz | 280.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| testsheet_xl-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 310.6 kB
Release files / testsheet_xl-0.1.0.tar.gz
| Download URL | testsheet_xl-0.1.0.tar.gz |
|---|---|
| Size | 280.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
53e20f33f326c4e5bbd6d1b64809b30fd08a0d003c082d1950b2b82f8b4a9fc7
|
|
BLAKE2b-256 checksum How to use checksums |
29e57458dc8c753dd7ba3a88b57a64f7d65e2c3a5b4006d79bc301f86caaa030
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.6
|
Release files / testsheet_xl-0.1.0-py3-none-any.whl
| Download URL | testsheet_xl-0.1.0-py3-none-any.whl |
|---|---|
| Size | 30.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
acb3a531ebdc608fae8f540f0e45d034070b1890df8b7f16e5b24651aa52aec5
|
|
BLAKE2b-256 checksum How to use checksums |
312297e16559f449acedb4a958a37aef50657b54fb0586297c2d904fef5a5e72
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.6
|