Skip to content

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 25 Apr 09:25
· 45 commits to refs/heads/main since this release
6840a92

csvzen 0.2.0 introduces csvzen-test-kit — a zio-test integration providing golden-file (snapshot) testing for CSV encoder output.

To the author's knowledge, no other Scala CSV library ships golden testing. scala-csv, kantan-csv and zio-blocks/schema-csv all leave it to you to roll your own. csvzen 0.2.0 closes that gap, and the API mirrors zio-json-golden so anyone coming from the JSON side gets a familiar workflow with no relearning.

🤔 Why golden tests for CSV?

CSV output is a wire format. Once consumers exist — a downstream pipeline, a partner who imports the file daily, an Excel sheet someone wired up two years ago — your encoder's output is a contract. Any change in the bytes the encoder produces is a change in that contract: a column reordered, a date format tweaked, a CRLF turned into LF, a None rendered as "" instead of "null". Each is the kind of "harmless cleanup" that quietly breaks a consumer in production three weeks later.

A golden test is a small, opinionated answer:

  1. You commit a reference file — the golden — that captures what the encoder produces today for a representative set of inputs.
  2. On every test run, the encoder is re-executed against the same inputs and the output is compared byte-for-byte to the golden.
  3. If anything in the encoder's output changes, the test fails and shows you the diff.

The value comes from what it forces:

  • No silent format changes. Refactoring a CsvFieldEncoder, swapping a date library, "fixing" a quoting rule — all of it surfaces as a failing test with a visible diff, not as a wire-format regression that ships.
  • Cheap to write, dense in coverage. One csvGoldenTest(gen) call exercises 50 rows of randomised but stable input through the entire encoder stack. You don't write per-field assertions; you commit one file.
  • The diff is the spec. When the change is intentional, you open the _changed.csv next to the original, eyeball the diff to confirm the delta is what you wanted, and rename it over the original. The PR review then has a one-file diff that says exactly how the wire format moved.
  • Catches the boring stuff for free. Line-terminator drift, accidental quoting of a previously-unquoted column, an extra trailing newline, an Instant formatter that started emitting +00:00 instead of Z — all surface immediately, not at 3 AM in production.

The cost is one checked-in file per encoder shape and one rename when the contract intentionally changes. Worth it.

✨ Usage

libraryDependencies += "com.guizmaii" %% "csvzen-test-kit" % "0.2.0" % Test
import com.guizmaii.csvzen.core.*
import com.guizmaii.csvzen.testkit.*
import zio.test.*

object UserSpec extends ZIOSpecDefault {
  final case class User(id: Int, name: String, active: Boolean) derives CsvRowEncoder

  val gen: Gen[Sized, User] =
    for {
      id     <- Gen.int
      name   <- Gen.alphaNumericString
      active <- Gen.boolean
    } yield User(id, name, active)

  override def spec = suite("UserSpec")(
    csvGoldenTest(gen)
  )
}

Workflow

  • First run → writes src/test/resources/golden/User_new.csv and fails the test with "Remove _new from the suffix and re-run." That promotes the snapshot.
  • Subsequent runs → encoder output is compared byte-for-byte to the on-disk file. On mismatch a <Name>_changed.csv is written next to the original so you can diff. If the change is intentional, overwrite the original; if not, the test caught a regression.
  • No env-var auto-update mode. Promotion is always an explicit file rename.
  • On a passing run, leftover _changed.csv / _new.csv files are best-effort deleted so the workspace converges to clean.

Configuration

csvGoldenTest(
  gen,
  GoldenConfiguration(
    relativePath = "users",   // golden lives at src/test/resources/golden/users/User.csv
    sampleSize   = 50,        // default — bump for wider coverage
    csvConfig    = CsvConfig(delimiter = '\t', lineTerminator = "\n"),
  ),
)

GoldenConfiguration is a regular default-valued parameter — no implicit-config juggling at the call site.

📚 Docs

🛠 Other changes

  • CsvRowEncoder.derived scaladoc now flags the -Xmax-inlines requirement for case classes with ~25+ fields. Bump it in your build if you hit the "Maximal number of successive inlines (32) exceeded" compile error:
    scalacOptions ++= Seq("-Xmax-inlines:128")
  • CI plumbing fix for the release-drafter workflow (#6).

📦 Installation

libraryDependencies += "com.guizmaii" %% "csvzen-core"     % "0.2.0"
libraryDependencies += "com.guizmaii" %% "csvzen-test-kit" % "0.2.0" % Test

Targets Scala 3.3.7. JVM-only. csvzen-core has no runtime dependencies beyond the standard library; csvzen-test-kit pulls in zio-test, zio-test-magnolia, and zio for its compile-scope contract.

🙏 Acknowledgements

csvzen-test-kit is modelled directly on zio-json-golden — same workflow, same _new.csv / _changed.csv suffix dance, same GoldenConfiguration shape. Credit to the zio-json contributors for the design; csvzen-test-kit is the CSV-shaped translation.

Full diff: v0.1.0...v0.2.0