Skip to content

Add ZIO Test integration (difflicious-zio-test) - #350

Draft
Sootopolis wants to merge 2 commits into
jatcwang:masterfrom
Sootopolis:zio-test-integration
Draft

Sootopolis wants to merge 2 commits into
jatcwang:masterfrom
Sootopolis:zio-test-integration

Conversation

@Sootopolis

@Sootopolis Sootopolis commented Nov 1, 2026 •

Copy link
Copy Markdown

This adds ZIO Test support, following up on our chat after your London Scala talk. It needs no changes to ZIO Test.

object MySpec extends ZIOSpecDefault with ZIODiffliciousSpec {
  def spec = suite("MySpec")(
    test("a == b")(differ.assertNoDiff(a, b)),
  )
}

What a failure looks like

From the personal project mentioned under "Verification", with expectations broken on purpose. In the test output:

- a join after a membership inferred from matches → Rejoined, with when those matches were played
  ✗ Test id: 01M5ZMKSX820JS3QXX3SH0R14Y
    List(
      Rejoined(
        ts: 2025-07-01T00:00:00Z,
        previous: Inferred(
          firstMatch: 2025-06-01T00:00:00Z,
          lastMatch: 2025-06-02T00:00:00Z -> 2025-07-01T00:00:00Z
        )
      )
    )
  assertNoDiff(obtained, expected)
  at src/test/scala/ccas/analysis/apps/membership/TestMembershipAppPure.scala:119

Another failure, in diffliciousViewer --plain:

Failure 2: ccas.analysis.apps.membership.TestMembershipAppPure / classifyFromDb / a row a closed account hides, last seen in range → AccountClosed then, not LeftClub
Run id: 01M5ZMMH3SRESVEVYM5DM1T27J
Test id: 01M5ZMMV5NEYBZT97D50NRCK0E
Location: src/test/scala/ccas/analysis/apps/membership/TestMembershipAppPure.scala:195
Difflicious diff result: different
Summary: 1 non-ignored change(s).

Differences:
1. $[0] - type_mismatch (AccountClosed != LeftClub)
   obtained: AccountClosed( ts: 2025-06-02T00:00:00Z, newStatus: Closed )
   expected: LeftClub( ts: 2025-06-02T00:00:00Z )

What's being added

  • A difflicious-zio-test module for Scala 2.13 and 3, on the JVM, Scala.js and Scala Native. Reports are written on the JVM only, as with the other integrations.
  • ZIODiffliciousSpec, which gives a spec assertNoDiff and reports.
  • A Differ for zio.NonEmptyChunk, which those specs get without an import. zio.Chunk already works, as it's a Seq.
  • Unit tests, a scripted test that checks the report end to end, a docs page, and a demo in example.

How it differs from MUnit and Weaver

assertNoDiff returns a failed result instead of throwing, as ZIO Test's assertTrue does. Throwing broke two things: property tests (check) stopped shrinking, and in assertTrue(a) && differ.assertNoDiff(x, y) the exception discarded the failure that assertTrue returned. The cost is ZIO Test's usual one: a result left on a line of its own is ignored (-Wnonunit-statement warns about it).

Reports are written by an "aspect". ZIO Test's test(...) and suite(...) just build a tree of values, so there's no suite class to hook into. But a spec can list aspects, which ZIO Test applies to the whole tree before running it. ZIODiffliciousSpec adds one that picks up suite names on the way down and writes a report when a test fails on a diff.

The report gets the full path, e.g. outer / inner / my test, as ScalaTest's do, minus a root suite named after the spec, which the viewer would otherwise show twice.

Replacing a spec's aspects is common in ZIO code, and doing so drops this one. The test still fails and prints its diff, but no report is written, so the trait exposes it as protected def diffliciousReporting for such specs to add back. The docs cover this.

Changes to the shared build

Scala.js and Native test programs now link one at a time. With ZIO added, overlapping links took a full sbt test past the 3 GB in .jvmopts (master alone was fine). In three cold CI runs of master with and without this setting, the test step took about 7½ minutes either way. Raising the heap instead works too, if you prefer.

Global / concurrentRestrictions += Tags.limitSum(1, ScalaJSTags.Link, NativeTags.Link)

waitForMdocOutput is no longer cached. sbt 2 caches tasks by default, so when CI restores its cache and the build is unchanged, the wait is skipped and the website build can fail with "Invalid sidebar file". I reproduced it on master in my fork's CI by re-running a passed build (log). This has nothing to do with ZIO, so I can move it to its own PR if you prefer.

Verification

  • sbt 'zioTest3/test; zioTest/test'
  • sbt 'sbtPlugin/scripted difflicious/zio-test-jsonl'
  • sbt scalafmtCheckAll
  • Full CI on my fork: runs
  • On Scala.js and Native, CI can log No tests were executed or a low test count. That's a zio-test-sbt logging bug on passing runs, not skipped tests: a deliberately failing assertion on Native fails the build.
  • Tried it on a personal project: failed diffs opened in the viewer, and its 1,577 tests still pass.

Left out for now

These can follow if you'd like them:

  • An importable assertNoDiff. As with MUnit and Weaver, it needs the trait mixed in.
  • Differs for types from zio-json, zio-http and zio-prelude, which would need their own modules.
  • Running the scripted tests in CI. No integration's scripted test runs there today, so I ran this one by hand.

Generated with Claude Code

Sootopolis and others added 2 commits November 1, 2026 21:24
Adds a difflicious-zio-test module so ZIO Test users can call
differ.assertNoDiff(obtained, expected) in their specs.

- assertNoDiff returns a failing TestResult instead of throwing, so it
  combines with &&, works in for comprehensions, and keeps shrinking in
  property tests (check).
- On the JVM, ZIODiffliciousSpec adds a test aspect that writes a JSONL
  report for each failed diff, recording the labels of the enclosing
  suites as the test hierarchy. A root suite named after the spec is
  left out, since the report already records the spec's name. A diff is
  reported only when zio-test counts it towards the failure, matching
  what the console prints. Specs that replace `aspects` can add the
  aspect back through the protected diffliciousReporting. JS and Native
  get the assertion only.
- A Differ for zio.NonEmptyChunk. Specs that mix in ZIODiffliciousSpec
  get it without an import; elsewhere it comes from
  difflicious.ziotest.implicits.
- Cross-built for Scala 2.13 and 3 on JVM, JS and Native.
- Unit tests, a zio-test-jsonl scripted test, a docs page, and a demo in
  the example module.
- Link Scala.js and Scala Native test programs one at a time, so the
  full build stays within the 3 GB heap set in .jvmopts. ZIO's test
  programs are the largest in the build, and overlapping links pushed it
  out of memory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sbt 2 caches tasks by default. When CI restores sbt's disk cache and the
build definition is unchanged, the cached result skips the wait, so the
website build can start before all mdoc output files exist and fail
with "Invalid sidebar file".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Sootopolis
Sootopolis force-pushed the zio-test-integration branch from 264ae76 to c508f7c Compare November 1, 2026 21:25
@Sootopolis
Sootopolis marked this pull request as draft November 1, 2026 23:58

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant