Approval is a Java library which will make you look at your testing from a whole different angle¶
Approval provides a powerful toolkit of ways to test the behavior of critical components so you can prevent problems in your production environment.
What can it be used for?¶
Approval can be used for verifying objects that require more than a simple assert. The idea is that you sometimes just want to verify a particular result at the end and then start implementation refactoring. I like to call it “I will know the right result when I see it”. Usecases for this might be:
- performance improvements to the implementation while preserving the current system output
- just verifying RESTful response results, be it JSON, XML, HTML whatever
- people use it for TDD but instead of providing the result upfront you just implement the simple possible thing, verify the result and then start improving the implementation.
Getting Started¶
Getting Started will guide you through the process of testing your classes with approval testing. Don’t worry if you are used to normal testing with assertions you will get up to speed in minutes.
Setting Up Maven¶
Just add the approval
library as a dependency:
<dependencies>
<dependency>
<groupId>com.github.nikolavp</groupId>
<artifactId>approval</artifactId>
<version>${approval.version}</version>
</dependency>
</dependencies>
Warning
Make sure you have a approval.version
property declared in your POM with the current version,
which is 0.2.
How is approval testing different¶
There are many sources from which you can learn about approval testing(just google it) but basically the process is the following:
- you already have a working implementation of the thing you want to test
- you run it and get the result the first time
- the result will be shown to you in your preferred tool(this can be configured)
- you either approve the result in which case it is recored(saved) and the test pass or you disapprove it in which case the test fails
- the recorded result is then used on further test runs to make sure that there are no regressions in your code(i.e. you broke something and the result is not the same).
- Of course sometimes you want to change the way something behaves so if the result is not the same we will prompt you with difference between the new result and the last recorded again in your preferred tool.
Approvals utility¶
This is the main starting point of the library. If you want to just approve a primitive object or arrays of primitive object then you are ready to go. The following will start the approval process for a String
that MyCoolThing
(our class under test) generated and use src/test/resources/approval/string.verified
for recording/saving the results:
@Test
public void testMyCoolThingReturnsProperString() {
String result = MyCoolThing.getComplexMultilineString();
Approvals.verify(result, Paths.get("src", "resources", "approval", "result.txt"));
}
Approval class¶
This is the main object for starting the approval
process. Basically it is used like this:
@Test
public void testMyCoolThingReturnsProperStringControlled() {
String string = MyCoolThing.getComplexMultilineString();
Approval<String> approver = Approval.of(String.class)
.withReporter(Reporters.console())
.build();
approver.verify(string, Paths.get("src", "resources", "approval", "string.verified"));
}
note how this is different from Approvals utility - we are building a custom Approval
object which allows us to control and change the whole approval process. Look at Reporter class and Converter for more info.
Note
Approval object are thread safe so you are allowed to declare them as static variables and reuse them in all your tests. In the example above if we have more testing methods we can only declare the Approval
object once as a static variable in the Test class
Reporter class¶
Reporters(in lack of better name) are used to prompt the user for approving the result that was given to the Approval
object. There is a withReporter
method on ApprovalBuilder
that allows you to use a custom reporter. We provide some ready to use reporters in the following classes:
Reporters
- this factory class contains cross platform programs/reporters. Here you will find things likegvim
,console
.WindowsReporters
- this factory class contains Windows programs/reporters. Here you will find things likenotepadPlusPlus
,beyondCompare
,tortoiseText
.MacOSReporters
- this factory class contains MacOS programs/reporters. Here you will find things likediffMerge
,ksdiff
, etc.
Note
Sadly I am unable to properly test the windows and macOS reporters because I mostly have access to Linux machines. If you find a problem, blame it on me.
Converter¶
Converters are objects that are responsible for serializing objects to raw form(currently byte[]
). This interface allows you to create a custom converter for your custom objects and reuse the approval process in the library. We have converters for all primitive types, String and their array variants. Of course providing a converter for your custom object is dead easy. Let’s say you have a custom entity class that you are going to use for verifications in your tests:
package com.nikolavp.approval.example;
public class Entity {
private String name;
private int age;
public Entity(String name, int age) {
this.age = age;
this.name = name;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
Here is a possible simple converter for the class:
package com.nikolavp.approval.example;
import com.nikolavp.approval.converters.Converter;
import javax.annotation.Nonnull;
import java.nio.charset.StandardCharsets;
public class EntityConverter implements Converter<Entity> {
@Nonnull
@Override
public byte[] getRawForm(Entity value) {
return ("Entity is:\n" +
"age = " + value.getAge() + "\n" +
"name = " + value.getName() + "\n").getBytes(StandardCharsets.UTF_8);
}
}
now let’s say we execute a simple test
Entity entity = new Entity("Nikola", 30);
Approval<Entity> approver = Approval.of(Entity.class)
.withReporter(Reporters.console())
.withConveter(new EntityConverter())
.build();
approver.verify(entity, Paths.get("src/test/resources/approval/example/entity.verified"));
}
}
we will get the following output in the console(because we are using the console reporter)
Entity is:age = 30name = Nikola
Path Mapper¶
Path mapper are used to abstract the way in which the final path file that contains the verification result is built. You are not required to use them but if you want to add structure to the your approval files you will at some point find the need for them. Let’s see an example:
You have the following class containing two verifications:
package com.nikolavp.approval.example;
import com.nikolavp.approval.Approval;
import com.nikolavp.approval.reporters.Reporters;
import org.junit.Test;
import java.nio.file.Paths;
public class PathMappersExample {
private static final Approval<String> APPROVER = Approval.of(String.class)
.withReporter(Reporters.console())
.build();
@Test
public void shoulProperlyTestString() throws Exception {
APPROVER.verify("First string test", Paths.get("src", "test", "resources", "approvals", "first-test.txt"));
}
@Test
public void shoulProperlyTestStringSecond() throws Exception {
APPROVER.verify("Second string test", Paths.get("src", "test", "resources", "approvals", "second-test.txt"));
}
}
now if you want to add another approval test you will need to write the same destination directory for the approval path again. You can of course write a private static method that does the mapping for you but we can do better with PathMappers:
package com.nikolavp.approval.example;
import com.nikolavp.approval.Approval;
import com.nikolavp.approval.pathmappers.ParentPathMapper;
import com.nikolavp.approval.reporters.Reporters;
import org.junit.Test;
import java.nio.file.Paths;
public class PathMappersExampleImproved {
private static final Approval<String> APPROVER = Approval.of(String.class)
.withReporter(Reporters.console())
.withPathMapper(new ParentPathMapper<String>(Paths.get("src", "test", "resources", "approvals")))
.build();
@Test
public void shoulProperlyTestString() throws Exception {
APPROVER.verify("First string test", Paths.get("first-test.txt"));
}
@Test
public void shoulProperlyTestStringSecond() throws Exception {
APPROVER.verify("Second string test", Paths.get("second-test.txt"));
}
}
we abstracted the common parent directory with the help of the ParentPathMapper
class. We provide other path mapper as part of the library that you can use:
Limitations¶
Some things that you have to keep in mind when using the library:
- unordered objects like HashSet, HashMap cannot be determisticly verified because their representation will vary from run to run.
User Manual¶
Simple example of the library¶
Let’s try to test the simplest example possible:
package com.nikolavp.approval.example;
public class SimpleExample {
public static String generateHtml(String pageTitle) {
return String.format(
"<!DOCTYPE html>\n" +
"<html lang=\"en\">\n" +
"<head>\n" +
" <title>%s</title>\n" +
"<meta charset=\"utf-8\"/>\n" +
"<link href=\"css/myscript.css\"\n" +
" rel=\"stylesheet\"/>\n" +
"<script src=\"scripts/myscript.js\">\n" +
"</script>\n" +
"</head>\n" +
"<body>\n" +
"...\n" +
"</body>\n" +
"</html>", pageTitle);
}
}
now this class is not rocket science and if we want to test getResult(), we would write something like the following in JUnit:
package com.nikolavp.approval.example;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat;
public class SimpleExampleTest {
@Test
public void shouldReturnSomethingToTestOut() throws Exception {
//arrange
String title = "myTitle";
String expected = "<!DOCTYPE html>\n" +
"<html lang=\"en\">\n" +
"<head>\n" +
" <title>" + title +"</title>\n" +
"<meta charset=\"utf-8\"/>\n" +
"<link href=\"css/myscript.css\"\n" +
" rel=\"stylesheet\"/>\n" +
"<script src=\"scripts/myscript.js\">\n" +
"</script>\n" +
"</head>\n" +
"<body>\n" +
"...\n" +
"</body>\n" +
"</html>";
//act
String actual = SimpleExample.generateHtml(title);
//assert
assertThat(actual, equalTo(expected));
}
}
this is quite terse and short. Can we do better? Actually because we support strings out of the box, approval is a lot shorter
package com.nikolavp.approval.example;
import com.nikolavp.approval.Approvals;
import org.junit.Test;
import java.nio.file.Paths;
public class SimpleExampleApprovalTest {
@Test
public void shouldReturnSomethingToTestOut() throws Exception {
//assign
String title = "myTitle";
//act
String actual = SimpleExample.generateHtml(title);
//verify
Approvals.verify(actual, Paths.get("test.txt"));
}
}
when the latter is executed you will be prompted in your tool of choice to verify the result from getResult(). Verifying the result will vary from your tool of choice because some of them allow you to control the resulting file and others just show you what was the verification object.
To see it in action we will look at two possible reporters:
Gedit¶
Gedit is just a simple editor. When we run the test it will show us the string representation:

as you can see this is the string representation of the result opened in gedit. If we close gedit we will prompted by a confirm window which will ask us if we approve the result or it is not OK. On not OK the test will fail with an AssertionError
and otherwise the test will pass and will continue to pass until the returned value from getResult() changes.
GvimDiff¶
Gvimdiff is much more powerful than gedit. If we decide to use it then we got the power in our hands and we can decide if we want the file or not(there will be no confirmation window). Here is how it looks like:

as you can see on the left side is the result from the test run and on the right side is what will be written for consecutive test runs. If we are ok with the result we can get everything from the left side, save the right side and exit vim. The test will now pass and will continue to pass until the returned value from getResult() changes.
Let’s say someone changes the code and it no longer contains a DOCTYPE declaration. The reporter will fire up and we will get the following window:

we can approve the change or exit our tool and mark the result as invalid.
FAQ¶
Javadoc¶
com.nikolavp.approval¶
Approval¶
-
public class
Approval
<T>¶ The main entry point class for each approval process. This is the main service class that is doing the hard work - it calls other classes for custom logic based on the object that is approved. Created by nikolavp on 1/29/14.
Parameters: - <T> – the type of the object that will be approved by this
Approval
- <T> – the type of the object that will be approved by this
Constructors¶
-
Approval
(Reporter reporter, Converter<T> converter, PathMapper<T> pathMapper)¶ Create a new object that will be able to approve “things” for you.
Parameters: - reporter – a reporter that will be notified as needed for approval events
- converter – a converter that will be responsible for converting the type for approval to raw form
- pathMapper – the path mapper that will be used
-
Approval
(Reporter reporter, Converter<T> converter, PathMapper<T> pathMapper, com.nikolavp.approval.utils.FileSystemUtils fileSystemReadWriter)¶ This ctor is for testing only.
Methods¶
-
PathMapper<T>
getPathMapper
()¶
-
public static <T> ApprovalBuilder<T>
of
(Class<T> clazz)¶ Create a new approval builder that will be able to approve objects from the specified class type.
Parameters: - clazz – the class object for the things you will be approving
- <T> – the type of the objects you will be approving
Returns: an approval builder that will be able to construct an
Approval
for your objects
Approval.ApprovalBuilder¶
-
public static final class
ApprovalBuilder
<T>¶ A builder class for approvals. This is used to conveniently build new approvals for a specific type with custom reporters, converters, etc.
Parameters: - <T> – the type that will be approved by the the resulting approval object
Methods¶
-
public ApprovalBuilder<T>
withConveter
(Converter<T> converterToBeUsed)¶ Set the converter that will be used when building new approvals with this builder.
Parameters: - converterToBeUsed – the converter that will be used from the approval that will be built
Returns: the same builder for chaining
See also:
Converter
-
public ApprovalBuilder<T>
withPathMapper
(PathMapper<T> pathMapperToBeUsed)¶ Set a path mapper that will be used when building the path for approval results.
Parameters: - pathMapperToBeUsed – the path mapper
Returns: the same builder for chaining
-
public ApprovalBuilder<T>
withReporter
(Reporter reporterToBeUsed)¶ Set the reporter that will be used when building new approvals with this builder.
Parameters: - reporterToBeUsed – the reporter that will be used from the approval that will be built
Returns: the same builder for chaninig
See also:
Reporter
Approvals¶
-
public final class
Approvals
¶ Approvals for primitive types. This is a convenient static utility class that is the first thing to try when you want to use the library. If you happen to be lucky and need to verify only primitive types or array of primitive types then we got you covered.
User: nikolavp (Nikola Petrov) Date: 07/04/14 Time: 11:38
Methods¶
-
public static void
verify
(double[] doubles, Path path)¶ An overload for verifying double arrays. This will call the approval object with proper reporter and use the path for verification.
Parameters: - doubles – the double array that needs to be verified
- path – the path in which to store the approval file
-
public static void
verify
(boolean[] booleans, Path path)¶ An overload for verifying boolean arrays. This will call the approval object with proper reporter and use the path for verification.
Parameters: - booleans – the boolean array that needs to be verified
- path – the path in which to store the approval file
-
public static void
verify
(String[] strings, Path path)¶ An overload for verifying string arrays. This will call the approval object with proper reporter and use the path for verification.
Parameters: - strings – the string array that needs to be verified
- path – the path in which to store the approval file
-
public static void
verify
(double value, Path path)¶ An overload for verifying a single double value. This will call the approval object with proper reporter and use the path for verification.
Parameters: - value – the double that needs to be verified
- path – the path in which to store the approval file
-
public static void
verify
(boolean value, Path path)¶ An overload for verifying a single boolean value. This will call the approval object with proper reporter and use the path for verification.
Parameters: - value – the boolean that needs to be verified
- path – the path in which to store the approval file
-
public static void
verify
(String value, Path path)¶ An overload for verifying a single String value. This will call the approval object with proper reporter and use the path for verification.
Parameters: - value – the String that needs to be verified
- path – the path in which to store the approval file
FullPathMapper¶
-
public interface
FullPathMapper
<T>¶ A mapper that unlike
PathMapper
doesn’t resolve the approval file path based on a given sub path but only needs the value. Of course there are possible implementations that don’n even need the value likecom.nikolavp.approval.pathmappers.JunitPathMapper
.Parameters: - <T> – the value that will be approved
See also:
PathMapper
PathMapper¶
-
public interface
PathMapper
<T>¶ An interface representing objects that will return an appropriate path for the approval process. Most of the times those are used because you don’t want to repeat yourself with the same parent path in
com.nikolavp.approval.Approval.verify(Object,java.nio.file.Path)
for the path argument. This will map your approval results file from the value for approval and a possible sub path.Parameters: - <T> – the value that will be approved
See also:
com.nikolavp.approval.pathmappers.ParentPathMapper
Methods¶
-
Path
getPath
(T value, Path approvalFilePath)¶ Gets the path for the approval result based on the value that we want to approve and a sub path for that.
Parameters: - value – the value that will be approved
- approvalFilePath – a name/subpath for the approval. This will be the path that was passed to
Approval.verify(Object,java.nio.file.Path)
Returns: the full path for the approval result
Reporter¶
-
public interface
Reporter
¶ Created by nikolavp on 1/30/14.
Methods¶
-
void
approveNew
(byte[] value, File fileForApproval, File fileForVerification)¶ Called by an
com.nikolavp.approval.Approval
object when a value for verification is produced but no old.Parameters: - value – the new value that came from the verification
- fileForApproval – the approval file(this contains the value that was passed in)
- fileForVerification – the file for the this new approval value @return true if the new value is approved and false otherwise
-
boolean
canApprove
(File fileForApproval)¶ A method to check if this reporter is supported for the following file type or environment! Reporters are different for different platforms and file types and this in conjuction with
com.nikolavp.approval.reporters.Reporters.firstWorking
will allow you to plug different reporters for different environments(CI, Windows, Linux, MacOS, etc).Parameters: - fileForApproval – the file that we want to approve
Returns: true if we can approve the file and false otherwise
-
void
notTheSame
(byte[] oldValue, File fileForVerification, byte[] newValue, File fileForApproval)¶ Called by an
com.nikolavp.approval.Approval
object when values don’t match in the approval process.Parameters: - oldValue – the old value that was found in fileForVerification from old runs
- newValue – the new value that was passed for verification
- fileForVerification – the file for this approval value
- fileForApproval – the file for the new content
com.nikolavp.approval.converters¶
AbstractConverter¶
AbstractStringConverter¶
-
public abstract class
AbstractStringConverter
<T> extends AbstractConverter<T>¶ A convenient abstract converter to handle object approvals on string representable objects.
Parameters: - <T> – the type you want to convert
Methods¶
-
protected abstract String
getStringForm
(T value)¶ Gets the string representation of the type object. This representation will be written in the files you are going to then use in the approval process.
Parameters: - value – the object that you want to convert
Returns: the string representation of the object
ArrayConverter¶
-
public class
ArrayConverter
<T> extends AbstractStringConverter<T[]>¶ An array converter that uses another converter for it’s items. This allows this converter to be composed with another one and allow you to convert your types even if they are in an array. User: nikolavp Date: 20/03/14 Time: 19:34
Parameters: - <T> – The type of the items in the list that this converter accepts
Constructors¶
Converter¶
-
public interface
Converter
<T>¶ A converter interface. Converters are the objects in the approval system that convert your object to their raw form that can be written to the files. Note that the raw form is not always a string representation of the object. If for example your object is an image. User: nikolavp Date: 28/02/14 Time: 14:47
Parameters: - <T> – the type you are going to convert to raw form
Converters¶
-
public final class
Converters
¶ Converters for primitive types. Most of these just call toString on the passed object and then get the raw representation of the string result. . User: nikolavp Date: 28/02/14 Time: 17:25
DefaultConverter¶
-
public class
DefaultConverter
implements Converter<byte[]>¶ Just a simple converter for byte array primitives. We might want to move this into
Converters
. User: nikolavp Date: 28/02/14 Time: 14:54
ListConverter¶
-
public class
ListConverter
<T> extends AbstractStringConverter<List<T>>¶ A list converter that uses another converter for it’s items. This allows this converter to be composed with another one and allow you to convert your types even if they are in a list. User: nikolavp Date: 28/02/14 Time: 17:47
Parameters: - <T> – The type of the items in the list that this converter accepts
Constructors¶
ReflectiveBeanConverter¶
-
public class
ReflectiveBeanConverter
<T> extends AbstractStringConverter<T>¶ A converter that accepts a bean object and uses reflection to introspect the fields of the bean and builds a raw form of them. Note that the fields must have a human readable string representation for this converter to work properly. User: nikolavp Date: 28/02/14 Time: 15:12
Parameters: - <T> – the type of objects you want convert to it’s raw form
com.nikolavp.approval.pathmappers¶
JunitPathMapper¶
-
public class
JunitPathMapper
implements TestRule, PathMapper, FullPathMapper¶ A path mapper that have to be declared as a
org.junit.Rule
and will use the standard junit mechanisms to put your approval results in $package-name-with-slashes/$classname/$methodname.This class can be used as a
com.nikolavp.approval.PathMapper
in which case it will put your approval results in that directory or you can use it as acom.nikolavp.approval.FullPathMapper
in which case your approval result for the single virifacion will be put in a file with that path. In the latter case you will have to make sure that there aren’t two approvals for a single test method.
Constructors¶
ParentPathMapper¶
-
public class
ParentPathMapper
<T> implements PathMapper<T>¶ A path mapper that will put all approvals in a common parent path. Let’s say you want to put all your approval results in src/test/resources/approval(we assume a common maven directory structure) then you can use this mapper as follows:
Approval approval = Approval.of(String.class) .withPathMapper(new ParentPathMapper(Paths.get("src/test/resources/approval"))) .build();
now the following call
approval.verify("Some cool string value", Paths.get("some_cool_value.txt");
will put the approved value in the file src/test/resources/approval/some_cool_value.txt
Parameters: - <T> – the value that will be approved
Constructors¶
com.nikolavp.approval.reporters¶
AbstractReporter¶
ExecutableDifferenceReporter¶
-
public class
ExecutableDifferenceReporter
implements Reporter¶ A reporter that will shell out to an executable that is presented on the user’s machine to verify the test output. Note that the approval command and the difference commands can be the same.
- approval command will be used for the first approval
- the difference command will be used when there is already a verified file but it is not the same as the value from the user
Constructors¶
FirstWorkingReporter¶
MacOSReporters¶
-
public final class
MacOSReporters
¶ Reporters that use macOS specific binaries, i.e. not cross platform programs.
If you are looking for something cross platform like gvim, emacs, you better look in
com.nikolavp.approval.reporters.Reporters
.
Reporters¶
-
public final class
Reporters
¶ Created with IntelliJ IDEA. User: nikolavp Date: 10/02/14 Time: 15:10 To change this template use File | Settings | File Templates.
Methods¶
-
public static Reporter
console
()¶ Creates a simple reporter that will print/report approvals to the console. This reporter will use convenient command line tools under the hood to only report the changes in finds. This is perfect for batch modes or when you run your build in a CI server
Returns: a reporter that uses console unix tools under the hood
-
public static Reporter
imageMagick
()¶ A reporter that compares images. Currently this uses imagemagick for comparison. If you only want to view the new image on first approval and when there is a difference, then you better use the
fileLauncher()
reporter which will do this for you.Returns: the reporter that uses ImagemMagick for comparison
SwingInteractiveReporter¶
-
public class
SwingInteractiveReporter
implements Reporter¶ A reporter that can wrap another reporter and give you a prompt to approve the new result value.
This is useful for reporters that cannot give you a clear way to create the result file. If for example you are using a reporter that only shows you the resulting value but you cannot move it to the proper result file for the approval.
If you say OK/YES on the prompt then the result will be written to the proper file for the next approval time.
Constructors¶
-
SwingInteractiveReporter
(Reporter other, FileSystemUtils fileSystemReadWriter)¶
WindowsReporters¶
-
public final class
WindowsReporters
¶ Reporters that use windows specific binaries, i.e. the programs that are used are not cross platform.
If you are looking for something cross platform like gvim, emacs, you better look in
com.nikolavp.approval.reporters.Reporters
.
Methods¶
-
public static Reporter
beyondCompare
()¶ A reporter that calls Beyond Compare 3 to show you the results.
Returns: a reporter that calls beyond compare
-
public static Reporter
tortoiseImage
()¶ A reporter that calls TortoiseIDiff to show you the results.
Returns: a reporter that calls tortoise image difference tool
-
public static Reporter
tortoiseText
()¶ A reporter that calls TortoiseMerge to show you the results.
Returns: a reporter that calls tortoise difference tool for text
WindowsReporters.WindowsExecutableReporter¶
-
public static class
WindowsExecutableReporter
extends ExecutableDifferenceReporter¶ Windows executable reporters should use this class instead of the more general ExecutableDifferenceReporter.
Constructors¶
com.nikolavp.approval.utils¶
CrossPlatformCommand¶
-
public abstract class
CrossPlatformCommand
<T>¶ A command that when run will execute the proper method for the specified operating system. This is especially useful when you are trying to create for handling different platforms. Here is an example usage of the class:
final Boolean result = new CrossPlatformCommand<Boolean>() { @Override protected Boolean onWindows() { //do your logic for windows } @Override protected Boolean onUnix() { //do your logic for unix } @Override protected Boolean onMac() { //do your logic for mac } @Override protected Boolean onSolaris() { //do your logic for solaris } }.execute();
Parameters: - <T> – the result from the command.
Methods¶
-
public T
execute
()¶ Main method that should be executed. This will return the proper result depending on your platform.
Returns: the result
-
public static boolean
isMac
()¶ Check if the current OS is MacOS.
Returns: true if macOS and false otherwise
-
public static boolean
isSolaris
()¶ Check if the current OS is Solaris.
Returns: true if solaris and false otherwise
-
public static boolean
isUnix
()¶ Check if the current OS is some sort of unix.
Returns: true if unix and false otherwise
DefaultFileSystemUtils¶
-
public class
DefaultFileSystemUtils
implements com.nikolavp.approval.utils.FileSystemUtils¶ A default implementation for
com.nikolavp.approval.utils.FileSystemUtils
. This one just delegates to methods inFiles
. User: nikolavp Date: 27/02/14 Time: 12:26