pedrovgs / kotlinsnapshot Goto Github PK
View Code? Open in Web Editor NEWThis project forked from gaumala/kotlinsnapshot
Snapshot Testing framework for Kotlin.
License: MIT License
This project forked from gaumala/kotlinsnapshot
Snapshot Testing framework for Kotlin.
License: MIT License
We will be using ktlint
First of all, thank you very much for this super cool lib, really good work as always :). Karumi stays strong ๐ช :P.
I got the following test:
@Test
fun showConversationsOnSuccessfulLoading() {
givenConversationsOnBothDataSources(anyIndividualConversations())
viewModel.viewState.observeForever(viewStateObserver)
viewModel.viewState.value.matchWithSnapshot()
}
Test should pass and store the snapshot on its first run.
I'm getting the following error (which is expected and well described).
Kotlin Snapshot library couldn't find the name of the test. Review if the test case file or the spec file contains the word test or spec or specify a snapshot name manually, this is a requirement needed to use Kotlin Snapshot.
This is due to my test suite being called ConversationsEndToEndShould
. So the explicit error message was helpful and accurate. I've fixed it by naming the snapshot, like:
viewModel.viewState.value.matchWithSnapshot("Conversations loaded")
I've not digged yet on how you've implemented the test indexing but It'd be nice if it wasn't depending on the test or test class names that much (in case it's possible), or alternatively add a rule for the "should" substring.
"Should" is kind of used in tests broadly since it gives a good naming when you run them, like:
That's used more often in java probably, since Kotlin already supports using human readable names with spaces and so on in tests, but still it'd be a nice to have.
SerializationModule
SerializationModule
0.3.0
The current implementation of this library (1.0.0) does not format the string we save as a serialized representation of any instance we want to use as part of our assertions. This might end up with a really large string saved into a file where a human developer would like to take a look at.
As this testing strategy might require a human validation at some point we should make the string format we save easier to read.
For example, see https://github.com/approvals.
Please consider changing terminology in the project.
The diff_match_patch
library should be included as a regular gradle dependency instead of hardcoded in the project.
Right now, the library is included manually inside the project
When using KotlinSnapshot with a data class with private fields, these atributes should be or not be ignored but printed using the associated value and not an empty space.
The library prints an empty space for every private field.
Create a class with private attribute and use the library.
1.0.0
Removed the template because this is a feature request rather than a bug report.
It would be useful to have an option to fail an assertion, rather than generate a snapshot, if a snapshot is missing. This is especially useful for CI. It's also possible I'm missing something here but in local dev with the latest version of KotlinSnapshot, just deleting a snapshot will recreate it on the next test, which could create false negatives (no failure? false positive success?).
Jest's default behavior is not to create snapshots without --updateSnapshots
which I think is safe, but even just a flag would be useful.
Please point me in the right direction if I'm missing something, which is totally possible, but it looks like the answer is currently no:
We can create a new extension to get all the information stored in Room automatically, every stored value right to a JSON/XML format we can later on compare.
Can reference to KotlinSnapshot
from androidTest
source sets
Cannot reference to KotlinSnapshot
from androidTest
source sets
androidTest
: https://stackoverflow.com/posts/34397590/revisionsandroidTest
KotlinSnapshot
to the test2.2.0
./gradlew purgeSnapshots
should update the snapshot
The snapshot test runs and fails without updating the snapshot. In the end I followed the instructions from https://github.com/GAumala/KotlinSnapshot which luckily still works.
Execute ./gradlew purgeSnapshots
2.2.0
As a new developer contributing, it's hard to follow how the Gradle setup works in this repo.
Let's document this: https://github.com/Karumi/KotlinSnapshot/blob/bfd3cbfd6fbe07b0d028475a091d363ac72b2466/KotlinConsumer/build.gradle#L5
Explain it, talk about it, ext in a markdown file somewhere.
If the snapshots folder is not specified we should review where these files are placed. Right now they are saved as part of the root folder but I'd personally prefer to choose the test folder instead. We should review this and implement the change before the first release.
Release com.karumi.kotlinsnapshot 0.1.0
./gradlew updateSnapshots
updates snapshots for failing tests
./gradlew updateSnapshots
says tests are failing and no action is taken
I'm using the library inside of a Junit 5, Spring @WebMvcTest() test.
I had a test failing due to a snapshot.
Running updateSnapshots
only tells me the test is failing. No action is taken.
2.2.0
The library API should be simpler to use.
We need to instantiate a Camera
instance for every test class and this could be easier to use with some Kotlin magic.
First not released version ๐
2.0.0
We can create a new extension to get the state of the shared preferences automatically, every stored value right to a JSON/XML format we can later on compare.
When enabling testClassAsDirectory = true
and you add a snapshot name manually like snap.matchWithSnapshot(anyObject, "snapshot name")
, It should add a folder with the class name and add a file with the name snapshot name.snap
testClassAsDirectory = true
but when you add a custom snapshot name, it isn't creating any folder with the test class name.
val snap = KotlinSnapshot(testClassAsDirectory = true)
snap.matchWithSnapshot(anyObject, "snapshot name")
1.0.0
.snap
files inside the test class name directory, then you can group the .snap
files into folders.__snapshot__
directory0.3.0
How are new versions published? Let's document it.
Maybe even add it to the CI?
Either of the cases below produces a snapshot:
class SomeTest : StringSpec({
"Base Test" {
val value = 1
value.matchWithSnapshot()
val kotlinSnapshot =
KotlinSnapshot(snapshotsFolder = "src/test/kotlin/com/test", testClassAsDirectory = true)
kotlinSnapshot.matchWithSnapshot(value)
}
})
Kotlin Snapshot library couldn't find the name of the test. Review if the test case file or the spec file contains the word test or spec or specify a snapshot name manually, this is a requirement needed to use Kotlin Snapshot
com.karumi.kotlinsnapshot.exceptions.TestNameNotFoundException: Kotlin Snapshot library couldn't find the name of the test. Review if the test case file or the spec file contains the word test or spec or specify a snapshot name manually, this is a requirement needed to use Kotlin Snapshot
at com.karumi.kotlinsnapshot.core.Camera.extractTestCaseName(Camera.kt:120)
val kotestVersion = "4.4.3"
testImplementation("io.kotest:kotest-runner-junit5-jvm:$kotestVersion")
2.3.0
7.X
Would be nice to know:
Deserialization of Untrusted Data [High Severity][https://security.snyk.io/vuln/SNYK-JAVA-COMGOOGLECODEGSON-1730327] in com.google.code.gson:[email protected]
It should serialize and generate the snapshot file
An error is thrown:
com.google.gson.JsonParseException: cannot serialize java.util.Arrays$ArrayList; did you forget to register a subtype?
call matchwithSnapshot()
on an object that has a list attribute
E.g.
data class TestObject(val list: List<String> = listOf("a", "b"))
@Test
fun test() {
TestObject().matchWithSnapshot()
}
2.2.2
Running a test like this one locally, the test passes without any problem:
@Test
fun showConversationsOnSuccessfulLoading() {
givenConversationsOnBothDataSources(anyIndividualConversations())
viewModel.viewState.observeForever(viewStateObserver)
viewModel.viewState.value.matchWithSnapshot()
}
When you run it on CI (CircleCI) it starts failing. You can probably use any other CI systems.
Salida
com.karumi.kotlinsnapshot.core.SnapshotException: ?[31mReceived value?[0m does not match ?[32mstored snapshot: "Conversations loaded.snap"?[0m
?[32m-Snapshot?[0m
?[31m+Received?[0m
ConversationsViewState(conversations={
?[32m- jueves, ene
?[0m?[31m+ Thursday, Jan
?[0m 1=[IndividualConversation(agentJoinTimes={}, agents=[Author(bio=Any bio text, firstName=Any name, lastName=Any lastname, location=Any location, photoUrl=https://anyphotoservice.com/anyphoto, userId=Any id), Author(bio=Any bio text, firstName=Any name, lastName=Any lastname, location=Any location, photoUrl=https://anyphotoservice.com/anyphoto, userId=Any id), Author(bio=Any bio text, firstName=Any name, lastName=Any lastname, location=Any location, photoUrl=https://anyphotoservice.com/anyphoto, userId=Any id)], conversationId=clientConversation1, mostRecentElement=Left(a=Message(author=Author(bio=null, firstName=Client, lastName=www.client.com/photoUrl, location=null, photoUrl=null, userId=Client), conversationId=correlationId, date=Date(, ), direction=OUTGOING, messageId=Conversation1, readStatus=Read(readTime=Date(, )), status=PENDING_SEND, text=Text1), , ), status=Open, unreadCount=0), IndividualConversation(agentJoinTimes={}, agents=[Author(bio=Any bio text, firstName=Any name, lastName=Any lastname, location=Any location, photoUrl=https://anyphotoservice.com/anyphoto, userId=Any id), Author(bio=Any bio text, firstName=Any name, lastName=Any lastname, location=Any location, photoUrl=https://anyphotoservice.com/anyphoto, userId=Any id), Author(bio=Any bio text, firstName=Any name, lastName=Any lastname, location=Any location, photoUrl=https://anyphotoservice.com/anyphoto, userId=Any id)], conversationId=clientConversation2, mostRecentElement=Left(a=Message(author=Author(bio=null, firstName=Client, lastName=www.client.com/photoUrl, location=null, photoUrl=null, userId=Client), conversationId=correlationId, date=Date(, ), direction=OUTGOING, messageId=Conversation1, readStatus=Read(readTime=Date(, )), status=PENDING_SEND, text=Text2), , ), status=Open, unreadCount=0), IndividualConversation(agentJoinTimes={}, agents=[Author(bio=Any bio text, firstName=Any name, lastName=Any lastname, location=Any location, photoUrl=https://anyphotoservice.com/anyphoto, userId=Any id), Author(bio=Any bio text, firstName=Any name, lastName=Any lastname, location=Any location, photoUrl=https://anyphotoservice.com/anyphoto, userId=Any id), Author(bio=Any bio text, firstName=Any name, lastName=Any lastname, location=Any location, photoUrl=https://anyphotoservice.com/anyphoto, userId=Any id)], conversationId=clientConversation3, mostRecentElement=Left(a=Message(author=Author(bio=null, firstName=Client, lastName=www.client.com/photoUrl, location=null, photoUrl=null, userId=Client), conversationId=correlationId, date=Date(, ), direction=OUTGOING, messageId=Conversation1, readStatus=Read(readTime=Date(, )), status=PENDING_SEND, text=Text3), , ), status=Open, unreadCount=0), IndividualConversation(agentJoinTimes={}, agents=[Author(bio=Any bio text, firstName=Any name, lastName=Any lastname, location=Any location, photoUrl=https://anyphotoservice.com/anyphoto, userId=Any id), Author(bio=Any bio text, firstName=Any name, lastName=Any lastname, location=Any location, photoUrl=https://anyphotoservice.com/anyphoto, userId=Any id), Author(bio=Any bio text, firstName=Any name, lastName=Any lastname, location=Any location, photoUrl=https://anyphotoservice.com/anyphoto, userId=Any id)], conversationId=clientConversation4, mostRecentElement=Left(a=Message(author=Author(bio=null, firstName=Client, lastName=www.client.com/photoUrl, location=null, photoUrl=null, userId=Client), conversationId=correlationId, date=Date(, ), direction=OUTGOING, messageId=Conversation1, readStatus=Read(readTime=Date(, )), status=PENDING_SEND, text=Text4), , ), status=Open, unreadCount=0), IndividualConversation(agentJoinTimes={}, agents=[Author(bio=Any bio text, firstName=Any name, lastName=Any lastname, location=Any location, photoUrl=https://anyphotoservice.com/anyphoto, userId=Any id), Author(bio=Any bio text, firstName=Any name, lastName=Any lastname, location=Any location, photoUrl=https://anyphotoservice.com/anyphoto, userId=Any id), Author(bio=Any bio text, firstName=Any name, lastName=Any lastname, location=Any location, photoUrl=https://anyphotoservice.com/anyphoto, userId=Any id)], conversationId=clientConversation5, mostRecentElement=Left(a=Message(author=Author(bio=null, firstName=Client, lastName=www.client.com/photoUrl, location=null, photoUrl=null, userId=Client), conversationId=correlationId, date=Date(, ), direction=OUTGOING, messageId=Conversation1, readStatus=Read(readTime=Date(, )), status=PENDING_SEND, text=Text5), , ), status=Open, unreadCount=0)]}, showingError=false)
at com.karumi.kotlinsnapshot.core.Camera.matchValueWithExistingSnapshot(Camera.kt:48)
at com.karumi.kotlinsnapshot.core.Camera.matchWithSnapshot(Camera.kt:27)
at com.karumi.kotlinsnapshot.KotlinSnapshotKt.matchWithSnapshot(KotlinSnapshot.kt:28)
at com.banno.conversations.conversations.ConversationsEndToEndShould.showConversationsOnSuccessfulLoading(ConversationsEndToEndShould.kt:96)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
at org.junit.rules.RunRules.evaluate(RunRules.java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.mockito.internal.runners.DefaultInternalRunner$1.run(DefaultInternalRunner.java:79)
at org.mockito.internal.runners.DefaultInternalRunner.run(DefaultInternalRunner.java:85)
at org.mockito.internal.runners.StrictRunner.run(StrictRunner.java:39)
at org.mockito.junit.MockitoJUnitRunner.run(MockitoJUnitRunner.java:163)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:116)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:59)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:39)
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:66)
at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:51)
at sun.reflect.GeneratedMethodAccessor41.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:35)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:32)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:93)
at com.sun.proxy.$Proxy1.processTestClass(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.processTestClass(TestWorker.java:109)
at sun.reflect.GeneratedMethodAccessor40.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:35)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:146)
at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:128)
at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:404)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:46)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:55)
at java.lang.Thread.run(Thread.java:748)
If we remove the noise, the important part would be:
- jueves, ene
+ Thursday, Jan
The actual date is just a mock date in a nested depth into the object hierarchy, and it is created like Date(1L)
. It looks like it's correct but its being compared internally by the library using its pretty printed string representation in two different languages (localization). So we can discard this being a usual date / timezone problem.
If you use a standard approach on your test you would probably assert by using assertEquals
or eq
(nested equals), and it would pass without any problem, since equals compares the timestamps from both date objects (long). (The test was passing before integrating the lib). It's also significative that the test passes locally, probably because the locale being used is the one expected.
Digging a bit into the library code, I found out that you're serializing stuff just by calling toString()
in a nested way in the object hierarchy. toString()
doesn't look like the safest approach for serialization since each type implements it in different ways.
Regardless of using Kotlin and data classes (with their default toString over all its properties), this is a risky approach. Some types like Date()
have an implementation of toString()
that relies on things like the machine's Locale. Date#toString()
prints a date following the format: "EEE MMM dd HH:mm:ss zzz yyyy"
so it'll print a different value depending on the Locale.
I.e: for date serialization it would work better to serialize them as Long
timestamps, which will always match regardless of the timezone.
I would suggest you to use any existing java serialization libraries to json, protobuf or any other formats that are already covering all those issues, since you probably don't want to end up reinventing the serialization wheel for all the existent types. You can include them as implementation
so it's not exposed as a transitive dependency to client projects.
An interesting question to think about here would be: why was it serialized and stored in the snapshot using one language but then compared after deserialization in a later run using a different one? I have no clue, but both things were done by the same CI environment.
As a new developer contributing, it's hard to follow how the duplicate folder structure
For example, whats the difference between:
this file in /src/main and this file in core/bin ????
Let's add markdown documentation for this.
com.karumi.kotlinsnapshot.exceptions.TestNameNotFoundException: Kotlin Snapshot library couldn't find the name of the test. Review if the test case file or the spec file contains the word test or spec or specify a snapshot name manually, this is a requirement needed to use Kotlin Snapshot
at com.karumi.kotlinsnapshot.core.Camera.extractTestCaseName(Camera.kt:105)
at com.karumi.kotlinsnapshot.core.Camera.matchWithSnapshot(Camera.kt:26)
at com.karumi.kotlinsnapshot.KotlinSnapshotKt.matchWithSnapshot(KotlinSnapshot.kt:39)
at com.karumi.springbootkotlin.ExtensionsKt.matchWithSnapshot(extensions.kt:52)
StringIndexOutOfBoundsException
java.lang.StringIndexOutOfBoundsException: String index out of range: -1
at java.lang.String.substring(String.java:1967)
at com.karumi.kotlinsnapshot.core.Camera.extractTestCaseName(Camera.kt:88)
at com.karumi.kotlinsnapshot.core.Camera.matchWithSnapshot(Camera.kt:25)
at com.karumi.kotlinsnapshot.KotlinSnapshotKt.matchWithSnapshot(KotlinSnapshot.kt:17)
at com.karumi.kotlinsnapshot.KotlinSnapshotKt.matchWithSnapshot$default(KotlinSnapshot.kt:16)
at ItemTest.should_be_equals(ItemTest.kt:16)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Version 0.1.0
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.