Skip to content

astubbs/truth-generator

Repository files navigation

GitHub Workflow Status truth generator?style=plastic truth generator?style=plastic GitHub stackoverflow google‐truth 5555ff

Subject generator library for Google Truth.

Example
assertThat(person).hasAddress().hasStreet().ignoringCase().endsWith('Rd')

Inspired by the code generator for AssertJ.

  • Boiler plate and assertion generator system for Google Truth

  • Template generation so that Users can easily add assertions, be added to VCS for maintenance and automatic incorporation to future Subject tree generation runs

  • Chained assertion strategies (e.g. assertThat(person).hasAddress().hasStreet().ignoringCase().endsWith('Rd')

  • Recursive generation of Subject`s so you don’t have to register them yourself - including going into external and/or restricted modules (i.e. java’s `UUID, ZonedDateTime etc)

    • For example, you register only a single class with the plugin, and it will generate Subject’s for every single referenced class in the tree.

  • Generated assertion strategies for standard field types

  • Single convenient entry point for all managed `Subject`s

  • Extendable root Subjects from the Truth library injected into generated code, e.g.:

    • MyStringSubject : Equality check ignoring white space and line endings, file content equality

    • MyMapSubject : key content check and others

  • Subject’s for restricted packages automatically shaded (like java.* classes - UUID etc) Tree depth limits can potentially be added to limit the quantity of generated code.

  • Maven plugin (can make plugins for other systems if requested)

  • Dog foods - uses its own bootstrapped Subjects to test itself

  • Various ways to specify classes under test for generation in the plugin

  • Detection of existing user managed `Subject`s under version control - won’t regenerate, and will be injected into the generated structure

  • Support for legacy non bean compliant objects like Kafka’s ConsumerRecord

  • Collections, maps etc generic types are read and used in generated assertions

    • i.e. assertThat(Map<MyKey, ?> map)#containsKey(MyKey key)

  • Customisable extension points for base subjects (like list and map) which will apply to all types in generated tree (where generated entry points are used)

The plugin will generate template code that’s required by Truth to implement your own Subjects. This is mostly boilerplate and gets tiresome to write everytime you want to introduce a new Subject.

The library will also generate a lot of typical assertion methods, based on examining the classes under test.

Three Classes will be generated:

  • Parent

  • User Managed

  • Child

The role of these classes is:

  • The Parent class contains the generated assertions - more will be added as the library evolves.

  • The User Managed class is basically empty. Its purpose is to, if the user desires, to be a template to be added to with user created assertion methods, and brought into the users' projects' version control system.

  • The child class merges the two classes, and contains the factory and entry points for users' test code to bind into.

The one other class that is generated is the "Entry Pont" class, atm called ManagedTruth. This class contains a collection of all the assertThat entry points from all the generated classes. This is a convenience class so that the user can simply import ManagedTruth.assertThat, and will then access to all the assertion entry points for all generated classes.

Two directories are creates under 'target/generated-sources':

  • truth-assertions-managed - contains files that are not to be changed, and will evolve over time as features are added.

  • truth-assertions-templates - contains the templates for users to add their own assertion methods to. When you do - you should move it from the generated sources' dir, and put it under source control. If any of these types of classes detected upon further plugin runs, they will not be generated again and will be injected into the assertions hierarchy dynamically.

To see what happens when this is used, see the simple Example at the bottom of the document.

To see how this is being used in a real world problem, take a look at how it’s used in the Confluent Parallel Consumer library.

We’re not yet publishing to repo1, but we are publishing to both GitHub Packages and Package Cloud. This is because although GitHub is a giant and trustworthy, it does not allow anonymous access to it’s package system. Once must have set up an authentication token to use, so it’s a pain.

Package Cloud on the other hand works great, but some may not want to use it, in favour of GitHub. So for now, we publish to both.

Once repo1 publishing is set up, this won’t be necessary.

You can see what’s inside the repo, and what package we’re publishing.

  1. Simply add this repository to your build:

Package Cloud repository
<project>

...

    <repositories>
        <repository>
            <id>astubbs-truth-generator</id>
            <url>https://packagecloud.io/astubbs/truth-generator/maven2</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>astubbs-truth-generator</id>
            <url>https://packagecloud.io/astubbs/truth-generator/maven2</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>
</project>
  1. Setup your access token, with these instructions.

  2. Then add the following repository:

GitHub repository
<project>

...

    <repositories>
        <repository>
            <id>astubbs-truth-generator</id>
            <url>https://maven.pkg.github.com/astubbs/truth-generator</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>astubbs-truth-generator</id>
            <url>https://maven.pkg.github.com/astubbs/truth-generator</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>
</project>
Maven plugin inclusion
    <dependencies>
        <dependency>
            <groupId>io.stubbs.truth</groupId>
            <artifactId>truth-generator-api</artifactId>
            <scope>test</scope>
        </dependency>
        ... snip ...
    </dependencies>


... snip ...

    <build>
        <plugins>
            <plugin>
                <!-- mvn  io.stubbs.truth:truth-generator-maven-plugin:generate -->
                <groupId>io.stubbs.truth</groupId>
                <artifactId>truth-generator-maven-plugin</artifactId>
                <configuration>
                    <classes>
                        <param>io.stubbs.truth.generator.testModel.MyEmployee</param>
                    </classes>
                    <legacyClasses>
                        <param>io.stubbs.truth.generator.testing.legacy.NonBeanLegacy</param>
                    </legacyClasses>
                    <packages>
                        <package>io.stubbs.truth.generator.testModel.package</package>
                    </packages>
                    <entryPointClassPackage>io.stubbs.truth.extensions.tests.projectUnderTest</entryPointClassPackage>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            ... snip ...
        </plugins>
    </build>

Given a simple class Car below, with very few fields or referenced classes, the following classes are generated.

The class under test Car
@lombok.Value
public class Car {
    String name;
    Make make;
    int colourId;

    public enum Make {PLASTIC, METAL}
}
Generated Parent for Car
/**
 * Truth Subject for the {@link Car}.
 * <p>
 * Note that this class is generated / managed, and will change over time. So
 * any changes you might make will be overwritten.
 *
 * @see Car
 * @see CarSubject
 * @see CarChildSubject
 */
@Generated("truth-generator")
public class CarParentSubject extends Subject {

    protected final Car actual;

    protected CarParentSubject(FailureMetadata failureMetadata, io.stubbs.truth.generator.example.Car actual) {
        super(failureMetadata, actual);
        this.actual = actual;
    }

    /**
     * Returns the Subject for the given field type, so you can chain on other
     * assertions.
     */
    public IntegerSubject hasColourId() {
        isNotNull();
        return check("getColourId()").that(actual.getColourId());
    }

    /**
     * Simple check for equality for all fields.
     */
    public void hasColourIdNotEqualTo(int expected) {
        if (!(actual.getColourId() == expected)) {
            failWithActual(fact("expected ColourId NOT to be equal to", expected));
        }
    }

    /**
     * Simple check for equality for all fields.
     */
    public void hasColourIdEqualTo(int expected) {
        if ((actual.getColourId() == expected)) {
            failWithActual(fact("expected ColourId to be equal to", expected));
        }
    }

    /**
     * Returns the Subject for the given field type, so you can chain on other
     * assertions.
     */
    public MakeSubject hasMake() {
        isNotNull();
        return check("getMake()").about(makes()).that(actual.getMake());
    }

    /**
     * Simple check for equality for all fields.
     */
    public void hasMakeNotEqualTo(Make expected) {
        if (!(actual.getMake().equals(expected))) {
            failWithActual(fact("expected Make NOT to be equal to", expected));
        }
    }

    /**
     * Simple check for equality for all fields.
     */
    public void hasMakeEqualTo(io.stubbs.truth.generator.example.Car.Make expected) {
        if ((actual.getMake().equals(expected))) {
            failWithActual(fact("expected Make to be equal to", expected));
        }
    }

    /**
     * Returns the Subject for the given field type, so you can chain on other
     * assertions.
     */
    public MyStringSubject hasName() {
        isNotNull();
        return check("getName()").about(strings()).that(actual.getName());
    }

    /**
     * Simple check for equality for all fields.
     */
    public void hasNameNotEqualTo(java.lang.String expected) {
        if (!(actual.getName().equals(expected))) {
            failWithActual(fact("expected Name NOT to be equal to", expected));
        }
    }

    /**
     * Simple check for equality for all fields.
     */
    public void hasNameEqualTo(java.lang.String expected) {
        if ((actual.getName().equals(expected))) {
            failWithActual(fact("expected Name to be equal to", expected));
        }
    }
}
Generated user template Subject for Car - if you wanted to add your own methods, you would move this source file into VCS, then add them as you see git. It will automatically be used in future generator runs. The same goes for the Car.Make Subject.
/**
 * Optionally move this class into source control, and add your custom
 * assertions here.
 *
 * <p>
 * If the system detects this class already exists, it won't attempt to generate
 * a new one. Note that if the base skeleton of this class ever changes, you
 * won't automatically get it updated.
 *
 * @see Car
 * @see CarParentSubject
 */
@UserManagedSubject(Car.class)
@Generated("truth-generator")
public class CarSubject extends CarParentSubject {

	protected CarSubject(FailureMetadata failureMetadata, io.stubbs.truth.generator.example.Car actual) {
		super(failureMetadata, actual);
	}

	/**
	 * Returns an assertion builder for a {@link Car} class.
	 */
	public static Factory<CarSubject, Car> cars() {
		return CarSubject::new;
	}
}
Generated Child Subject for Car
/**
 * Entry point for assertions for @{Car}. Import the static accessor methods
 * from this class and use them. Combines the generated code from
 * {@CarParentSubject}and the user code from {@CarSubject}.
 *
 * @see io.stubbs.truth.generator.example.Car
 * @see CarSubject
 * @see CarParentSubject
 */
@Generated("truth-generator")
public class CarChildSubject extends CarSubject {

	/**
	 * This constructor should not be used, instead see the parent's.
	 *
	 * @see CarSubject
	 */
	private CarChildSubject(FailureMetadata failureMetadata, io.stubbs.truth.generator.example.Car actual) {
		super(failureMetadata, actual);
	}

	/**
	 * Entry point for {@link Car} assertions.
	 */
	public static CarSubject assertThat(io.stubbs.truth.generator.example.Car actual) {
		return Truth.assertAbout(cars()).that(actual);
	}

	/**
	 * Convenience entry point for {@link Car} assertions when being mixed with
	 * other "assertThat" assertion libraries.
	 *
	 * @see #assertThat
	 */
	public static CarSubject assertTruth(io.stubbs.truth.generator.example.Car actual) {
		return assertThat(actual);
	}
}
Generated Parent for Car.Make enum
/**
* Truth Subject for the {@link Make}.
*
* Note that this class is generated / managed, and will change over time. So
* any changes you might make will be overwritten.
*
* @see Make
* @see MakeSubject
* @see MakeChildSubject
*/
@Generated("truth-generator")
public class MakeParentSubject extends Subject {

	protected final Make actual;

	protected MakeParentSubject(FailureMetadata failureMetadata, Make actual) {
		super(failureMetadata, actual);
		this.actual = actual;
	}

	/**
	 * Returns the Subject for the given field type, so you can chain on other
	 * assertions.
	 */
	public ClassSubject hasDeclaringClass() {
		isNotNull();
		return check("getDeclaringClass()").that(actual.getDeclaringClass());
	}

	/**
	 * Simple check for equality for all fields.
	 */
	public void hasDeclaringClassNotEqualTo(java.lang.Class expected) {
		if (!(actual.getDeclaringClass().equals(expected))) {
			failWithActual(fact("expected DeclaringClass NOT to be equal to", expected));
		}
	}

	/**
	 * Simple check for equality for all fields.
	 */
	public void hasDeclaringClassEqualTo(java.lang.Class expected) {
		if ((actual.getDeclaringClass().equals(expected))) {
			failWithActual(fact("expected DeclaringClass to be equal to", expected));
		}
	}
}
Generated user template Subject for Car.Make
/**
* Optionally move this class into source control, and add your custom
* assertions here.
*
* <p>
* If the system detects this class already exists, it won't attempt to generate
* a new one. Note that if the base skeleton of this class ever changes, you
* won't automatically get it updated.
*
* @see Make
* @see MakeParentSubject
*/
@UserManagedSubject(Make.class)
@Generated("truth-generator")
public class MakeSubject extends MakeParentSubject {

	protected MakeSubject(FailureMetadata failureMetadata, Make actual) {
		super(failureMetadata, actual);
	}

	/**
	 * Returns an assertion builder for a {@link Make} class.
	 */
	public static Factory<MakeSubject, Make> makes() {
		return MakeSubject::new;
	}
}
Generated Child for Car.Make
/**
* Entry point for assertions for @{Make}. Import the static accessor methods
* from this class and use them. Combines the generated code from
* {@MakeParentSubject}and the user code from {@MakeSubject}.
*
* @see io.stubbs.truth.generator.example.Car$Make
* @see MakeSubject
* @see MakeParentSubject
*/
@Generated("truth-generator")
public class MakeChildSubject extends MakeSubject {

	/**
	 * This constructor should not be used, instead see the parent's.
	 *
	 * @see MakeSubject
	 */
	private MakeChildSubject(FailureMetadata failureMetadata, Make actual) {
		super(failureMetadata, actual);
	}

	/**
	 * Entry point for {@link Make} assertions.
	 */
	public static MakeSubject assertThat(io.stubbs.truth.generator.example.Car.Make actual) {
		return Truth.assertAbout(makes()).that(actual);
	}

	/**
	 * Convenience entry point for {@link Make} assertions when being mixed with
	 * other "assertThat" assertion libraries.
	 *
	 * @see #assertThat
	 */
	public static MakeSubject assertTruth(io.stubbs.truth.generator.example.Car.Make actual) {
		return assertThat(actual);
	}
}
Generated Access Point
/**
 * Single point of access for all managed Subjects.
 */
public class ManagedTruth {

	/**
	 * Entry point for {@link Make} assertions.
	 */
	public static MakeSubject assertThat(io.stubbs.truth.generator.example.Car.Make actual) {
		return Truth.assertAbout(makes()).that(actual);
	}

	/**
	 * Convenience entry point for {@link Make} assertions when being mixed with
	 * other "assertThat" assertion libraries.
	 *
	 * @see #assertThat
	 */
	public static MakeSubject assertTruth(io.stubbs.truth.generator.example.Car.Make actual) {
		return assertThat(actual);
	}

	/**
	 * Entry point for {@link Car} assertions.
	 */
	public static CarSubject assertThat(io.stubbs.truth.generator.example.Car actual) {
		return Truth.assertAbout(cars()).that(actual);
	}

	/**
	 * Convenience entry point for {@link Car} assertions when being mixed with
	 * other "assertThat" assertion libraries.
	 *
	 * @see #assertThat
	 */
	public static CarSubject assertTruth(io.stubbs.truth.generator.example.Car actual) {
		return assertThat(actual);
	}

}

When master branch is read to release: run the release workflow from GitHub actions. It will by default start the next dev version as an incremented patch version.

When ready to start a new major or minor version, use versions:set command.