{"id":15403620,"url":"https://github.com/rogervinas/snapshot-testing","last_synced_at":"2025-04-16T03:44:20.336Z","repository":{"id":60769625,"uuid":"544993847","full_name":"rogervinas/snapshot-testing","owner":"rogervinas","description":"📸 Snapshot Testing with Kotlin","archived":false,"fork":false,"pushed_at":"2025-03-26T13:29:56.000Z","size":281,"stargazers_count":2,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-29T04:43:02.809Z","etag":null,"topics":["kotlin","snapshot-testing","testing"],"latest_commit_sha":null,"homepage":"https://dev.to/rogervinas/snapshot-testing-with-kotlin-3nk6","language":"Kotlin","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rogervinas.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-10-03T15:43:37.000Z","updated_at":"2025-03-26T13:29:58.000Z","dependencies_parsed_at":"2023-11-29T18:28:20.308Z","dependency_job_id":"2c98223c-accf-4d78-af56-dcb994da6a6a","html_url":"https://github.com/rogervinas/snapshot-testing","commit_stats":{"total_commits":78,"total_committers":3,"mean_commits":26.0,"dds":0.3846153846153846,"last_synced_commit":"f9088d88e3de282fa70342c8d76ee0e1475d4c3e"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rogervinas%2Fsnapshot-testing","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rogervinas%2Fsnapshot-testing/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rogervinas%2Fsnapshot-testing/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rogervinas%2Fsnapshot-testing/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rogervinas","download_url":"https://codeload.github.com/rogervinas/snapshot-testing/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249191940,"owners_count":21227683,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["kotlin","snapshot-testing","testing"],"created_at":"2024-10-01T16:09:28.864Z","updated_at":"2025-04-16T03:44:20.329Z","avatar_url":"https://github.com/rogervinas.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![CI](https://github.com/rogervinas/snapshot-testing/actions/workflows/ci.yml/badge.svg)](https://github.com/rogervinas/snapshot-testing/actions/workflows/ci.yml)\n![Java](https://img.shields.io/badge/Java-21-blue?labelColor=black)\n![Kotlin](https://img.shields.io/badge/Kotlin-2.x-blue?labelColor=black)\n![JavaSnapshotTesting](https://img.shields.io/badge/JavaSnaphotTesting-4.0.8-blue?labelColor=black)\n![Selfie](https://img.shields.io/badge/Selfie-2.5.1-blue?labelColor=black)\n\n# Snapshot Testing with Kotlin\n\nSnapshot testing is a test technique where first time the test is executed the output of the function being tested is saved to a file, **the snapshot**, and future executions of the test will only pass if the function generates the very same output\n\nThis seems very popular in [the frontend community](https://jestjs.io/docs/snapshot-testing) but us backends we can use it too! I use it whenever I find myself manually saving test expectations as text files 😅\n\nIn this PoC we will use two different snapshot testing libraries JVM compatible:\n1. [**Java Snapshot Testing**](https://github.com/origin-energy/java-snapshot-testing) - [loved by lazy productive devs!](https://github.com/origin-energy/java-snapshot-testing#the-testing-framework-loved-by-lazy-productive-devs)\n2. [**Selfie**](https://github.com/diffplug/selfie) - [are you still writing assertions by hand?](https://thecontextwindow.ai/p/temporarily-embarrassed-snapshots)\n\nLet's start!\n\n* [Implementation to test](#implementation-to-test) - test results should be deterministic!\n* [Using Java Snapshot Testing](#using-java-snapshot-testing)\n  * [Serialize to JSON](#serialize-to-json)\n  * [Parameterized tests](#parameterized-tests)\n* [Using Selfie](#using-selfie)\n  * [Serialize to JSON](#serialize-to-json-1)\n  * [Parameterized tests](#parameterized-tests-1)\n\n## Implementation to test\n\nImagine that we have to test this simple [`MyImpl`](src/main/kotlin/org/rogervinas/MyImpl.kt):\n\n```kotlin\nclass MyImpl {\n\n  private val random = Random.Default\n\n  fun doSomething(input: Int) = MyResult(\n    oneInteger = input,\n    oneDouble = 3.7 * input,\n    oneString = \"a\".repeat(input),\n    oneDateTime = LocalDateTime.of(\n      LocalDate.of(2022, 5, 3),\n      LocalTime.of(13, 46, 18)\n    )\n  )\n  \n  fun doSomethingMore() = MyResult(\n    oneInteger = random.nextInt(),\n    oneDouble = random.nextDouble(),\n    oneString = \"a\".repeat(random.nextInt(10)),\n    oneDateTime = LocalDateTime.now()\n  )\n}\n\ndata class MyResult(\n  val oneInteger: Int,\n  val oneDouble: Double,\n  val oneString: String,\n  val oneDateTime: LocalDateTime\n)\n```\n\nNotice that:\n* `doSomething` function is testable as its results are deterministic ✅\n* `doSomethingMore` function is not testable as its results are random ❌\n\nSo first we need to change `doSomethingMore` implementation a little bit:\n\n```kotlin\nclass MyImpl(\n  private val random: Random,\n  private val clock: Clock\n) {\n    \n  fun doSomething() { }\n  \n  fun doSomethingMore() = MyResult(\n    oneInteger = random.nextInt(),\n    oneDouble = random.nextDouble(),\n    oneString = \"a\".repeat(random.nextInt(10)),\n    oneDateTime = LocalDateTime.now(clock)\n  )\n}\n```\n\nSo we can create instances of [`MyImpl`](src/main/kotlin/org/rogervinas/MyImpl.kt) for testing that will return deterministic results:\n\n```kotlin\nmyImplUnderTest = MyImpl(\n  random = Random(seed=1234),\n  clock = Clock.fixed(\n    Instant.parse(\"2022-10-01T10:30:00.000Z\"),\n    ZoneId.of(\"UTC\")\n  )\n)\n```\n\nAnd create instances of [`MyImpl`](src/main/kotlin/org/rogervinas/MyImpl.kt) for production:\n\n```kotlin\nmyImpl = MyImpl(\n  random = Random.Default, \n  clock = Clock.systemDefaultZone()\n)\n```\n\n## Using [Java Snapshot Testing](https://github.com/origin-energy/java-snapshot-testing)\n\nTo configure the library just follow the [Junit5 + Gradle quickstart](https://github.com/origin-energy/java-snapshot-testing#quick-start-junit5--gradle-example) guide:\n* Add required dependencies\n* Add required [`src/test/resources/snapshot.properties`](src/test/resources/snapshot.properties) file. It uses by default `output-dir=src/test/java` so snapshots are generated within the source code (I suppose so we don't forget to commit them to git) but I personally use `output-dir=src/test/snapshots` so snapshots are generated in its own directory\n\nWe can write our first snapshot test [`MyImplTestWithJavaSnapshot`](src/test/kotlin/org/rogervinas/MyImplTestWithJavaSnapshot.kt):\n\n```kotlin\n@ExtendWith(SnapshotExtension::class)\ninternal class MyImplTestWithJavaSnapshot {\n\n  private lateinit var expect: Expect\n\n  private val myImpl = MyImpl()\n\n  @Test\n  fun `should do something`() {\n    val myResult = myImpl.doSomething(7)\n    expect.toMatchSnapshot(myResult)\n  }\n}\n```\n\nIt will create a snapshot file [`MyImplTestWithJavaSnapshot.snap`](src/test/snapshots/org/rogervinas/MyImplTestWithJavaSnapshot.snap) with these contents:\n\n```text\norg.rogervinas.MyImplTestWithJavaSnapshot.should do something=[\nMyResult(oneInteger=7, oneDouble=25.900000000000002, oneString=aaaaaaa, oneDateTime=2022-05-03T13:46:18)\n]\n```\n\nAnd if we re-execute the test it will match against the saved snapshot\n\n### Serialize to JSON\n\nBy default, this library generates snapshots using the **ToString** serializer. We can use the **JSON** serializer instead:\n\n```kotlin\n@Test\nfun `should do something`() {\n  val myResult = myImpl.doSomething(7)\n  expect.serializer(\"json\").toMatchSnapshot(myResult)\n}\n```\n\nDon't forget to add the required `com.fasterxml.jackson.core` dependencies and to delete the previous snapshot\n\nThen the new snapshot file will look like:\n\n```text\norg.rogervinas.MyImplTestWithJavaSnapshot.should do something=[\n  {\n    \"oneInteger\": 7,\n    \"oneDouble\": 25.900000000000002,\n    \"oneString\": \"aaaaaaa\",\n    \"oneDateTime\": \"2022-05-03T13:46:18\"\n  }\n]\n```\n\nWe can also use our own custom serializers just providing in the `serializer` method one of the serializer class, the serializer instance or even the serializer name configured in [`snapshot.properties`](src/test/resources/snapshot.properties)\n\n### Parameterized tests\n\nWe can create parameterized tests using the `scenario` method:\n\n```kotlin\n@ParameterizedTest\n@ValueSource(ints = [1, 2, 3, 4, 5, 6, 7, 8, 9])\nfun `should do something`(input: Int) {\n  val myResult = myImpl.doSomething(input)\n  expect.serializer(\"json\").scenario(\"$input\").toMatchSnapshot(myResult)\n}\n```\n\nThis way each execution has its own snapshot expectation:\n\n```text\norg.rogervinas.MyImplTestWithJavaSnapshot.should do something[1]=[\n  {\n    \"oneInteger\": 1,\n    \"oneDouble\": 3.7,\n    \"oneString\": \"a\",\n    \"oneDateTime\": \"2022-05-03T13:46:18\"\n  }\n]\n\n...\n\norg.rogervinas.MyImplTestWithJavaSnapshot.should do something[9]=[\n  {\n    \"oneInteger\": 9,\n    \"oneDouble\": 33.300000000000004,\n    \"oneString\": \"aaaaaaaaa\",\n    \"oneDateTime\": \"2022-05-03T13:46:18\"\n  }\n]\n```\n\n## Using [Selfie](https://github.com/diffplug/selfie)\n\nTo configure the library follow [Installation](https://selfie.dev/jvm/get-started#installation) and [Quickstart](https://selfie.dev/jvm/get-started#quickstart) guides and just add required dependencies with no extra configuration\n\nWe can create our first snapshot test [`MyImplTestWithSelfie`](src/test/kotlin/org/rogervinas/MyImplTestWithSelfie.kt):\n\n```kotlin\ninternal class MyImplTestWithSelfie {\n  @Test\n  fun `should do something`() {\n    val myResult = myImpl.doSomething(7)\n    Selfie.expectSelfie(myResult.toString()).toMatchDisk()\n  }\n}\n```\n\nIt will create a snapshot file [`MyImplTestWithSelfie.ss`](src/test/kotlin/org/rogervinas/MyImplTestWithSelfie.ss) with these contents:\n\n```text\n╔═ should do something ═╗\nMyResult(oneInteger=7, oneDouble=25.900000000000002, oneString=aaaaaaa, oneDateTime=2022-05-03T13:46:18)\n```\n\nAnd if we re-execute the test it will match against the saved snapshot\n\nAnytime the snapshot does not match we will get a message with instructions on how to proceed:\n\n```text\nSnapshot mismatch / Snapshot not found\n- update this snapshot by adding `_TODO` to the function name\n- update all snapshots in this file by adding `//selfieonce` or `//SELFIEWRITE`\n```\n\n### Serialize to JSON\n\nIf instead of matching against `.toString()` we want to serialize to **JSON** we can customize a `Camera` and use it:\n\n```kotlin\nprivate val selfieCamera = Camera\u003cAny\u003e { actual -\u003e\n  val mapper = ObjectMapper()\n  mapper.findAndRegisterModules()\n  Snapshot.of(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(actual))\n}\n\n@Test\nfun `should do something`() {\n  val myResult = myImpl.doSomething(7)\n  Selfie.expectSelfie(myResult, selfieCamera).toMatchDisk()\n}\n```\n\nThen the new snapshot file will look like:\n\n```text\n╔═ should do something ═╗\n{\n  \"oneInteger\" : 7,\n  \"oneDouble\" : 25.900000000000002,\n  \"oneString\" : \"aaaaaaa\",\n  \"oneDateTime\" : [ 2022, 5, 3, 13, 46, 18 ]\n}\n```\n\n### Parameterized tests\n\nWe can use parameterized tests passing a value to identify each match:\n\n```kotlin\n@ParameterizedTest\n@ValueSource(ints = [1, 2, 3, 4, 5, 6, 7, 8, 9])\nfun `should do something`(input: Int) {\n  val myResult = myImpl.doSomething(input)\n  Selfie.expectSelfie(myResult, selfieCamera).toMatchDisk(\"$input\")\n}\n```\n\nThen snapshots will be saved this way:\n\n```text\n╔═ should do something/1 ═╗\n{\n  \"oneInteger\" : 1,\n  \"oneDouble\" : 3.7,\n  \"oneString\" : \"a\",\n  \"oneDateTime\" : [ 2022, 5, 3, 13, 46, 18 ]\n}\n\n...\n\n╔═ should do something/9 ═╗\n{\n  \"oneInteger\" : 9,\n  \"oneDouble\" : 33.300000000000004,\n  \"oneString\" : \"aaaaaaaaa\",\n  \"oneDateTime\" : [ 2022, 5, 3, 13, 46, 18 ]\n}\n```\n\nThanks and happy coding! 💙\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frogervinas%2Fsnapshot-testing","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frogervinas%2Fsnapshot-testing","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frogervinas%2Fsnapshot-testing/lists"}