Purpose of Snapshot Testing
Snapshots help figuring out whether the output of the modules covered by tests is changed, without doing tons of asserts!
When is it usefull?
It’s usefull for deterministic tests. That is, running the same tests multiple times on a component that has not changed should produce the same results every time. You’re responsible for making sure your generated snapshots do not include platform specific or other non-deterministic data.
A Json Snapshot test does not assert Java types. You can continue doing that with any other testing framework.
Based on facebook’s Jest framework
GitHub Repository
How to install using Maven
Add to your pom.xml dependencies section:
<dependency>
<groupId>io.github.json-snapshot</groupId>
<artifactId>json-snapshot</artifactId>
<version>1.0.17</version>
</dependency>
Usage
package com.example;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import java.util.Arrays;
import java.util.List;
import static io.github.jsonSnapshot.SnapshotMatcher.*;
import static io.github.jsonSnapshot.SnapshotUtils.*;
import io.github.jsonSnapshot.SnapshotCaptor;
@RunWith(MockitoJUnitRunner.class)
public class ExampleTest {
@Mock
private FakeObject fakeObject;
@BeforeClass
public static void beforeAll() {
start();
}
@AfterClass
public static void afterAll() {
validateSnapshots();
}
@Test // Snapshot any object
public void shouldShowSnapshotExample() {
expect("<any type of object>").toMatchSnapshot();
}
@Test // Snapshot arguments passed to mocked object (from Mockito library)
public void shouldExtractArgsFromMethod() {
fakeObject.fakeMethod("test1", 1L, Arrays.asList("listTest1"));
fakeObject.fakeMethod("test2", 2L, Arrays.asList("listTest1", "listTest2"));
expect(extractArgs(fakeObject, "fakeMethod", new SnapshotCaptor(String.class), new SnapshotCaptor(Long.class), new SnapshotCaptor(List.class)))
.toMatchSnapshot();
}
@Test // Snapshot arguments passed to mocked object support ignore of fields
public void shouldExtractArgsFromFakeMethodWithComplexObject() {
FakeObject fake = new FakeObject();
fake.setId("idMock");
fake.setName("nameMock");
//With Ignore
fakeObject.fakeMethodWithComplexObject(fake);
Object fakeMethodWithComplexObjectWithIgnore = extractArgs(
fakeObject, "fakeMethodWithComplexObject",
new SnapshotCaptor(Object.class, FakeObject.class, "name"));
Mockito.reset(fakeObject);
// Without Ignore of fields
fakeObject.fakeMethodWithComplexObject(fake);
Object fakeMethodWithComplexObjectWithoutIgnore = extractArgs(
fakeObject, "fakeMethodWithComplexObject",
new SnapshotCaptor(Object.class, FakeObject.class));
expect(fakeMethodWithComplexObjectWithIgnore, fakeMethodWithComplexObjectWithoutIgnore).toMatchSnapshot();
}
class FakeObject {
private String id;
private Integer value;
private String name;
void fakeMethod(String fakeName, Long fakeNumber, List<String> fakeList) {
}
void fakeMethodWithComplexObject(Object fakeObj) {
}
void setId(String id) {
this.id = id;
}
void setName(String name) {
this.name = name;
}
}
}
When the test runs for the first time, the framework will create a snapshot file named ExampleTest.snap
alongside with your test class. It should look like this:
com.example.ExampleTest.shouldShowSnapshotExample=[
"<any type of object>"
]
com.example.ExampleTest.shouldExtractArgsFromMethod=[
{
"FakeObject.fakeMethod": [
{
"arg0": "test1",
"arg1": 1,
"arg2": [
"listTest1"
]
},
{
"arg0": "test2",
"arg1": 2,
"arg2": [
"listTest1",
"listTest2"
]
}
]
}
]
com.example.ExampleTest.shouldExtractArgsFromFakeMethodWithComplexObject=[
{
"FakeObject.fakeMethodWithComplexObject": [
{
"arg0": {
"id": "idMock"
}
}
]
},
{
"FakeObject.fakeMethodWithComplexObject": [
{
"arg0": {
"id": "idMock",
"name": "nameMock"
}
}
]
}
]
Whenever it runs again, the expect
method argument will be automatically validated with the .snap
file. That is why you should commit every .snap
file created.
Inheritance
Test classes inheritance becames usefull with snapshot testing due to the fact that the assertions are variable following snasphots, instead of code. To make usage of this benefit you should be aware of the following:
Start SnapshotMatcher on child classes only:
package com.example;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import static io.github.jsonSnapshot.SnapshotMatcher.start;
import static io.github.jsonSnapshot.SnapshotMatcher.validateSnapshots;
public class SnapshotChildClassTest extends SnapshotSuperClassTest {
@BeforeClass
public static void beforeAll() {
start();
}
@AfterClass
public static void afterAll() {
validateSnapshots();
}
@Override
public String getName() {
return "anyName";
}
}
Super classes can have @Test defined, but you should make the class abstract.
package com.example;
import org.junit.Test;
import static io.github.jsonSnapshot.SnapshotMatcher.expect;
public abstract class SnapshotSuperClassTest {
public abstract String getName();
@Test
public void shouldMatchSnapshotOne() {
expect(getName()).toMatchSnapshot();
}
}