v0.2.0
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:
- You commit a reference file — the golden — that captures what the encoder produces today for a representative set of inputs.
- On every test run, the encoder is re-executed against the same inputs and the output is compared byte-for-byte to the golden.
- 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.csvnext 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
Instantformatter that started emitting+00:00instead ofZ— 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" % Testimport 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.csvand fails the test with "Remove_newfrom 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.csvis 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.csvfiles 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
- Project README → Golden tests — the "why you want them" preface plus a quickstart.
test-kit/README.md— full workflow, configuration knobs, and the gotchas aroundDeriveGen[String]+ Unicode bidi rendering.
🛠 Other changes
CsvRowEncoder.derivedscaladoc now flags the-Xmax-inlinesrequirement 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" % TestTargets 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