Skip to content

[OPIK-8181] [BE] refactor: centralise dataset item test assertions in DatasetItemAssertions - #8095

Merged
JetoPistola merged 3 commits into
mainfrom
danield/OPIK-8181-centralise-dataset-item-test-assertions
Sep 4, 2026
Merged

[OPIK-8181] [BE] refactor: centralise dataset item test assertions in DatasetItemAssertions#8095
JetoPistola merged 3 commits into
mainfrom
danield/OPIK-8181-centralise-dataset-item-test-assertions

Conversation

@JetoPistola

@JetoPistola JetoPistola commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Details

image

IGNORED_FIELDS_DATA_ITEM was declared twice — once in DatasetsResourceTest and again, independently, in ExperimentAggregatesIntegrationTest — while DatasetVersionResourceTest imported it across package boundaries from a sibling test class. Thirteen call sites then re-derived the same usingRecursiveFieldByFieldElementComparatorIgnoringFields(...) chain by hand. This introduces a DatasetItemAssertions helper under api/resources/utils/datasets/ that owns the constant and the assertion methods, following the existing TraceAssertions shape, and migrates every call site onto it.

  • The two declarations had already drifted. The ExperimentAggregatesIntegrationTest copy was missing runSummariesByExperiment, a later-added derived field it never picked up. Reconciled on the union (9 fields): both files compare DatasetItems returned by the same endpoints, so the omission was staleness rather than a legitimate difference. Verified by a full green run of that suite (145/145) — the wider ignore set does not mask any assertion there.
  • Five methods rather than one. Call sites use genuinely different terminators (isEqualTo, containsExactlyElementsOf, containsExactlyInAnyOrder, containsAll, single-item recursive), so one signature could not cover them without contorting callers. The constant stays public on the helper for the few bespoke chains — exactly what TraceAssertions does — which satisfies single-ownership without forcing every site through a method.
  • ignoredFieldsPlus("data") replaces a mutable ArrayList copy at the one call site that ignores an extra field and then asserts it separately, reusing the Stream.concat idiom already established in ExperimentTestAssertions.
  • Standardised the helper on (actual, expected) argument order — the dominant convention and AssertJ's own reading order. Two of the former local helpers took (expected, actual).
  • .agents/skills/opik-backend/testing.md now leads with helper-over-constant reuse and names the six helpers that exist, so the next author reaches for the helper instead of copying a comparator chain.

Test-only change: no production code. Assertion semantics are unchanged at every call site bar one, called out below.

Review follow-up — one place the union reconciliation did change coverage. Routing ExperimentAggregatesIntegrationTest through the shared helper newly ignored runSummariesByExperiment, which its local constant had not ignored on main. The two lists differed there for a reason: ExperimentAggregatesDAO's parity query omits assertions_array (the mapper guards this with size() > ASSERTIONS_INDEX and degrades to null), while DatasetItemVersionDAO includes it — so this is precisely the field that parity test exists to compare. assertDatasetItemsWithExperimentItems now asserts it explicitly alongside the existing experiment-items loop, restoring the coverage while keeping the shared helper. No fixture exercises it today (none create suite_assertion scores), so nothing was masked — the loss was future protection. Raised by the automated reviewer; the accompanying suggestion to add assertions_array to the production DAO query is deliberately out of scope for a test-only ticket.

Change checklist

  • User facing
  • Documentation update

Issues

  • OPIK-8181

AI-WATERMARK

AI-WATERMARK: yes

  • Tools: Claude Code
  • Model(s): Claude Opus 5
  • Scope: full implementation
  • Human verification: pending author review

Testing

Commands run (from apps/opik-backend):

  • mvn test -Dtest='ExperimentAggregatesIntegrationTest'145/145 pass, BUILD SUCCESS, re-run after adding the explicit runSummariesByExperiment parity assertion described above. This is the suite where the change is semantically meaningful.
  • mvn test -Dtest='DatasetsResourceTest,DatasetVersionResourceTest,ExperimentAggregatesIntegrationTest' — every migrated call site's class passes: all 21 DatasetVersionResourceTest nested classes (9 migrations), and the dataset-item classes in DatasetsResourceTest (4 migrations) at 163/163 — CreateDatasetItems, GetDatasetItem, GetDatasetItemsByDatasetId, DeleteDatasetItems, FindDatasetItemsWithExperimentItems, FindDatasetItemsWithExperimentItemsSortingTest, FindDatasetItemsWithExperimentItemsAssertionResults.
  • mvn compile -DskipTests — clean.
  • mvn spotless:check — clean.

Environment: local, Testcontainers (MySQL + ClickHouse + Redis + ZooKeeper), macOS.

Tests not passing locally, with reason: the dataset-level classes in DatasetsResourceTest (FindDatasets, CreateDataset, GetDataset, ApiKey, SessionTokenCookie, ExperimentSummaryEnrichment) fail with HTTP 500 in this environment. These are pre-existing and unrelated — confirmed by running DatasetsResourceTest$FindDatasets against an unmodified origin/main checkout, which fails 37/74 identically (same tests, same line, same 500). None of those classes are touched by this PR, and a test-only assertion refactor cannot produce server-side 500s. Relying on CI for a clean environment run of those classes.

Documentation

.agents/skills/opik-backend/testing.md — the partial-comparison guidance now states that an existing assertion helper method or class takes precedence over inlining a shared ignore-field constant, names the six helpers under api/resources/utils/, documents ignoredFieldsPlus for the extra-field case, and says to add a new helper rather than hoist a constant into a test class when more than one class needs the assertion. The hasSize example in the same section was updated to call the helper instead of showing a raw comparator chain.

Revised after review feedback: the one-declaration rule is now scoped to the comparisons a helper actually covers, and explicitly permits a call site whose contract genuinely differs to ignore a different set — provided it derives from the shared constant via ignoredFieldsPlus rather than rebuilding the list. DatasetsResourceProjectScopedTest is the real case that motivated this: its expected item keeps id == null while the API generates one, so its ignore set legitimately includes id. The section also now warns about the trap this PR itself hit — a field one comparison ignores and another asserts is a real difference, not drift, and folding it into the shared list quietly drops coverage.

… DatasetItemAssertions

IGNORED_FIELDS_DATA_ITEM was declared twice - in DatasetsResourceTest and,
independently, in ExperimentAggregatesIntegrationTest - and DatasetVersionResourceTest
imported it across package boundaries from a sibling test class. Thirteen call sites
then re-derived the same comparator chain by hand.

Introduce DatasetItemAssertions under api/resources/utils/datasets/, following the
TraceAssertions shape: it owns the ignore-field constant and exposes the assertion
methods the dataset tests need. Five methods cover the distinct shapes in use
(single item, list equality, ordered, unordered, subset), plus ignoredFieldsPlus for
the one call site that ignores an extra field and asserts it separately.

The two constant declarations had already drifted: the ExperimentAggregatesIntegrationTest
copy was missing runSummariesByExperiment. Reconciled on the union, since both files
compare DatasetItems from the same endpoints and the field is a later-added derived one
the second copy simply never picked up.

Test-only. No production code and no change to what the assertions assert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation java Pull requests that update Java code Backend tests Including test files, or tests related like configuration. 🟡 size/M labels Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

⏱️ pre-commit per-hook timing

Hook Description Result Duration
☕ spotless — java backend Format Java code 5.06s
Total (1 ran) 5.06s
⏭️ 43 skipped (no matching files changed)
Hook Description Result
🐍 trim trailing whitespace — python sdk Strip trailing whitespace ⏭️
🐍 fix end of files — python sdk Ensure files end in a newline ⏭️
🐍 ruff — python sdk Lint + autofix Python (ruff) ⏭️
🐍 ruff-format — python sdk Format Python code (ruff) ⏭️
🐍 mypy — python sdk Static type check ⏭️
🤖 trim trailing whitespace — optimizer Strip trailing whitespace ⏭️
🤖 fix end of files — optimizer Ensure files end in a newline ⏭️
🤖 check yaml — optimizer Validate YAML syntax ⏭️
🤖 check json — optimizer Validate JSON syntax ⏭️
🤖 check toml — optimizer Validate TOML syntax ⏭️
🤖 check for added large files — optimizer Block large files (>1MB) ⏭️
🔐 detect private key — optimizer Block committed private keys ⏭️
🤖 check for merge conflicts — optimizer Block merge-conflict markers ⏭️
🤖 check for case conflicts — optimizer Block case-only name clashes ⏭️
🤖 pyupgrade — optimizer Modernize Python syntax ⏭️
🤖 ruff — optimizer Lint + autofix Python (ruff) ⏭️
🤖 ruff-format — optimizer Format Python code (ruff) ⏭️
🤖 mypy — optimizer Static type check ⏭️
📓 nbstripout — optimizer notebooks Strip notebook output ⏭️
📝 markdownlint — optimizer Lint Markdown ⏭️
🔤 codespell — optimizer Fix common misspellings ⏭️
📊 radon cc — optimizer Cyclomatic-complexity gate ⏭️
📊 radon raw — optimizer Raw size metrics gate ⏭️
📊 xenon — optimizer Fail on complexity thresholds ⏭️
📊 lizard — optimizer Cyclomatic-complexity gate ⏭️
🧹 vulture — optimizer Find dead code ⏭️
🛡️ trim trailing whitespace — guardrails Strip trailing whitespace ⏭️
🛡️ fix end of files — guardrails Ensure files end in a newline ⏭️
🛡️ ruff — guardrails Lint + autofix Python (ruff) ⏭️
🛡️ ruff-format — guardrails Format Python code (ruff) ⏭️
🛡️ mypy — guardrails Static type check ⏭️
⚓ helm-docs Regenerate Helm chart README ⏭️
block non-public FE plugins Block non-public FE plugins ⏭️
🧪 pre-commit wrapper smoke tests Self-test the wrapper scripts ⏭️
🧪 rebaseline script tests Self-test the changelog re-baseline script ⏭️
🌐 eslint — frontend Lint + autofix JS/TS ⏭️
🌐 typecheck — frontend Whole-project tsc type check ⏭️
📘 eslint — typescript sdk Lint + autofix JS/TS ⏭️
📘 typecheck — typescript sdk Whole-project tsc type check ⏭️
⚙️ actionlint — github workflows Lint GitHub Actions workflows ⏭️
🐳 hadolint — dockerfiles Lint Dockerfiles ⏭️
🌈 zizmor — github workflows security Security-scan GitHub Actions workflows ⏭️
🛡️ semgrep — java backend sql Block SQL injection-prone string formatting ⏭️

Comment thread .agents/skills/opik-backend/testing.md Outdated
Routing ExperimentAggregatesIntegrationTest through the shared helper newly
ignored runSummariesByExperiment, which its local constant had not ignored.
The two lists differed there for a reason: ExperimentAggregatesDAO's parity
query omits assertions_array (the mapper guards it with size() > ASSERTIONS_INDEX
and degrades to null) while DatasetItemVersionDAO includes it, so this is exactly
the field the parity test exists to compare.

Assert it explicitly in assertDatasetItemsWithExperimentItems, keeping the shared
helper. No fixture creates suite_assertion scores today, so nothing was masked -
the loss was future protection.

Also scope the skill-doc one-declaration rule to helper-covered comparisons: a
call site whose contract genuinely differs may ignore a different set, provided it
derives from the shared constant via ignoredFieldsPlus. DatasetsResourceProjectScopedTest
is the real case - its expected item keeps id == null while the API generates one.
The section now also warns about the trap this branch hit, where a field one
comparison ignores and another asserts gets folded into a shared list.

Both raised by the automated reviewer on PR #8095.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@JetoPistola
JetoPistola marked this pull request as ready for review September 3, 2026 06:18
@JetoPistola
JetoPistola requested review from a team as code owners September 3, 2026 06:18
@CometActions

Copy link
Copy Markdown
Collaborator

No test needed here.

Test-only refactor: all four Java files are under src/test/java (the new DatasetItemAssertions helper plus its three call sites), and the fifth changed file is .agents/skills/opik-backend/testing.md. No production code changed, so there is no user- or API-visible behaviour for an e2e test to cover. Worth noting the one real behaviour change is inside the tests themselves: the shared IGNORED_FIELDS_DATA_ITEM adds runSummariesByExperiment, which ExperimentAggregatesIntegrationTest previously compared — you already re-assert it explicitly there, so the parity check is preserved.

Run

Advisory, from the QA test radar. Nothing here blocks this PR, and anything it proposes is a draft for review.

@andrescrz andrescrz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@JetoPistola
JetoPistola merged commit d53961c into main Sep 4, 2026
81 checks passed
@JetoPistola
JetoPistola deleted the danield/OPIK-8181-centralise-dataset-item-test-assertions branch September 4, 2026 09:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend documentation Improvements or additions to documentation java Pull requests that update Java code 🟡 size/M tests Including test files, or tests related like configuration.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants