A downloaded report matches its model
200 says a file arrived. A sheet model says it is the right report.
The situation
The application generates a monthly report as an .xlsx. A 200 on the download says a file arrived. It does not say the file has the right sheet, the right columns, valid values in every row or the project the test created.
The test downloads the workbook over the API and checks its content. The sample suite runs this journey in SheetsJourney.cs.
The code
Compose
The workbook is read as OpenXML, so it makes no difference whether the application wrote it with ClosedXML, EPPlus or anything else:
// Setup.cs: one line adds the document capability.
builder.AddSheets();
The test
Describe a row of the report once, with the rules every value must meet. The test creates a project the report must contain, downloads the report and reads it through the model:
| Column | Rule | Fails as |
|---|---|---|
Name | Unique | Duplicate cell refs listed |
Status | ^[a-z]+$ | Per-cell pattern failure |
Environments | >= 0 | Collected, not first-only |
[Sheet("Summary", HeaderRows = [1])]public sealed record ProjectReportRow([property: Column("Name", Unique = true)] string Name,[property: Column("Status", Pattern = "^[a-z]+$")] string Status,[property: Column("Environments", Min = 0)] int Environments);
Open(response) needs no file. It reads the workbook directly from the REST response. Should.MatchModel() checks the model's rules across every row, and Row(...) proves the report contains the project this test created.
What the trace shows
The operations land in the order the test caused them:
- the project creation's own requests,
- the report download as an
http.requestwith itshttp.responseobservation and, when capture is on, the workbook itself as a response artifact, sheets.openwith the sheet it read, thensheets.modelwith the declared columns, and oneassert.sheetsoperation per assertion.
Reading a cell or a range records a sheets.range observation, and SheetsCoverageCollector aggregates those. Opening a workbook is evidence, not coverage: a range is covered only when a read verified it. See Sheets.
In short, the trace reads in test order:
01 data fixture creates report-atlas
02 http.request GET monthly.xlsx -> http.response + workbook artifact
03 sheets.open Summary -> sheets.model ProjectReportRow
04 assert.sheets MatchModel -> assert.sheets column checks -> Row("report-atlas")
This is the sample suite's own run:
Variations
- Check the layout separately.
report.Should.MatchHeaders()compares the sheet's header row with the model's columns in declaration order, so a moved column fails without reading values. - A label/value sheet.
[Sheet("Summary", Kind = ProtoSheetKind.KeyValue)]with[Label("Total")]properties reads a two-column summary, andsummary.Column(s => s.Total).Should.Be(123.45m)reads one value. See Key-value sheets. - No model.
Sheet("Summary").Cell("B4").Should.Be(1200m)andRange("A4:B5").Should.Match(...)work on a workbook without a record, for a one-off check. - A file or a stream.
Open(path)andOpen(stream)take the same workbook from disk or memory.
What it does not prove
- Only OpenXML
.xlsxis supported. It does not read.xlsor CSV, and it does not write files. Should.MatchModel()collects all column failures into one result. A missing header fails when the model is read, naming it; broken column rules are collected, each with its cell reference, into one failure.- Formulas are cached values. Nothing is recalculated, and dates are detected from the cell's style, not a schema.
- Ranges are capped at 1,000,000 cells, and hidden sheets are skipped unless the options ask for them.
- Coverage is read-based. A column present in the file but never read is uncovered.