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:

  1. you already have a working implementation of the thing you want to test
  2. you run it and get the result the first time
  3. the result will be shown to you in your preferred tool(this can be configured)
  4. 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
  5. 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).
  6. 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:

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 = 30
name = 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.