This library and plugin for Maven generates source code for Hamcrest Matchers for Java records. The API of the generated matchers reflect the names of both the record itself as well as its components (i.e. fields), and provide facilities to incrementally constrain how specific you want to express what your expectations are.
This project is currently in its infancy, but should still be usable. You are most welcome to play around with it, and I appreciate any feedback you may have!
record Book (String title, List<Author> authors, int pageCount, Publisher publisher) { }Given you have defined the record above in your domain, this library can generate a BookMatcher which can be used in tests to assert on instances of Book like this:
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static your.domain.BookMatcher.aBook();
...
Book effectiveJava = // resolve the Effective Java book
assertThat(effectiveJava, is(aBook().withTitle("Effective Java").withAuthors(not(empty()))));
List<Book> effectiveSeries = // resolve Effective Xyz series of books
assertThat(effectiveSeries, everyItem(aBook().withTitle(containsString("Effective"))));The code generated by Record Matcher Generator is what you see being used as
aBook().withTitle("Effective Java").withAuthors(not(empty()))which is used to describe what you expect the Book to look like, and not more specific than what is appropriate for context of this particular test. The generated BookMatcher will also have methods to specify withPageCount(..) and withPublisher(..), according to the components in Book.
You can try the Maven plugin without configuring anything in an existing project (even multiproject) of yours like this:
mvn test-compile com.github.runeflobakk:record-matcher-maven-plugin::generateThe plugin attempts to discover records in a particular base package (more on this below). If the package is not present in your project, you are likely to get see the following error:
There was an error scanning for records in package(s) [the.attempted.pkg]: IllegalArgumentException 'One or more of the input resources are incorrect'. Ensure that the packages are correctly defined and exists.
You can specify a package (which includes sub-packages) to scan for records:
mvn test-compile com.github.runeflobakk:record-matcher-maven-plugin::generate\ -Drecordmatcher.scanPackages=pkg.in.your.project
Lastly, you can add target/generated-sources/record-matchers as a source folder in your IDE to access and use the generated Hamcrest Matchers. Read on for how to configure Record Matcher Generator as an integral part of your Maven project.
Likely, you want to use this via the Maven plugin, and this is how the minimal configuration of the plugin looks like in your pom.xml file:
<build>
<plugins>
<plugin>
<groupId>com.github.runeflobakk</groupId>
<artifactId>record-matcher-maven-plugin</artifactId>
<version>0.3.2</version> <!-- replace with any newer version -->
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
...No specific configuration of the plugin itself is strictly required, and the example above will discover all records present in the package (including sub-packages) named the same as the groupId of your project, generate corresponding Matcher classes for them, and put them in target/generated-test-sources/record-matchers in the corresponding packages as each record they match on.
Using the groupId of your project as the base package for the plugin to discover records may or may not suit the project you are working with, so you can change this in a <configuration> section for the plugin:
<plugin>
<groupId>com.github.runeflobakk</groupId>
<artifactId>record-matcher-maven-plugin</artifactId>
<version>0.3.2</version> <!-- replace with any newer version -->
<configuration>
<scanPackages>
com.base.service
<scanPackages>
</configuration>
<executions>
...The configuration above will discover any records in com.base.service and sub-packages and put them in target/generated-test-sources/record-matchers in the corresponding packages as each record they match on.
You may separate the execution binding and configuration using pluginManagement as you see fit.
Try generating some Matchers using the command mvn generate-test-sources, or even mvn record-matcher:generate to only run the plugin on an already built project.
You can also list several packages separated by comma, if you want:
<scanPackages>
com.base.pkg.first,
com.base.pkg.another
<scanPackages>You can also disable the scanning, and instead list the specific records you want to generate Matchers for:
<configuration>
<scanEnabled>false</scanEnabled>
<includes>
<include>your.domain.SomeRecord</include>
<include>your.domain.sub.SomeOtherRecord</include>
</includes>
</configuration>The configuration above will disable scanning, but you are free to leave scanning enabled, as both approaches can be used in tandem. This will generate the source code for specifically SomeRecordMatcher and SomeOtherRecordMatcher (substitute with your own record(s)), and their static factory methods SomeRecordMatcher.aSomeRecord() and SomeOtherRecordMatcher.aSomeOtherRecord() which should provide their APIs via method chaining; you get autocompletion by typing . after the static factory method.
See also the Complete configuration reference for more ways to configure the plugin.
The plugin will itself include the folder where it generates code as a test source root for the compiler used when building with Maven.
Eclipse with M2E (default included with the Eclipse distributions for Java development) will automatically include the additional folder with the generated source code as a test source folder for projects where the plugin is configured in your pom.xml file. Currently, generating the Matcher sources is not performed automatically as part of the automatic incremental build in Eclipse, so this must be done with e.g. mvn record-matcher:generate from the command line whenever needed.
To my knowledge, you need to specifically configure the inclusion of this folder in your pom.xml file for this folder to be recognized as a test source folder by IDEs such as IntelliJ. This can be done with build-helper-maven-plugin:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.6.1</version>
<executions>
<execution>
<id>include-record-matchers</id>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>target/generated-test-sources/record-matchers</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>Alternatively, you will need to manually add target/generated-test-sources/record-matchers as a test source folder for your project in your IDE.
After running a build, e.g. mvn generate-test-sources, you should be able to see the generated Matcher classes in your IDE (a refresh of the project may be required for the IDE to see the changes on your file system).
This is the most obvious one, and what the project is really made for. Asserting with Hamcrest Matchers are done with the assertThat(..) method.
Suppose you have this record returned somewhere in your code:
public record Person (UUID id, boolean isActive,
String givenName, Optional<String> middleName, String surname,
String primaryEmailAddress, List<String> allEmailAddresses) {}And you need to test a case which concerns whether a person is active or not (whatever that may mean in your domain). With a Hamcrest Matcher generated by Record Matcher Generator, this can be expressed like this:
import your.domain.Person;
import static your.domain.PersonMatcher.aPerson; //generated by record-matcher-generator
...
Person activePerson = //resolve the person expected to be active
assertThat(activePerson, aPerson().withIsActive(true));Now, you may ask what is the point of all this fancyness, instead of just writing assertEquals(true, activePerson.isActive()), see the test go green, and be on with your day? In the latter case, the only value which is known by the assertion infrastucture is a boolean, and the best it can do in case of a test failure is to say that “expected true, but hey, it was false”, which requires you to look at the code to know anything about what actually failed. In the prior case, the object from your domain is known by the assertion infrastructure, so it has the ability to also provide more context for a test failure message: - it was a Person which was not as expected - the property isActive was expected to be true, but was in fact false. - it may also include the whole state of the Person for context, which may in many cases provide clues about what happened, and you may not need to fork out a debugger to resolve where things have gotten mixed up. That is a lot more helpful than “expected true, but was false”.
While you should keep your mocking code as simple as possible, there are cases where your stubs may need a bit of “smartness” to affect their behavior. Mockito allows to use Hamcrest Matchers to distinguish method invocations and how they respond, without being more specific than necessary.
See MockitoHamcrest.
AssertJ has direct support for using Hamcrest Matchers as Conditions via HamcrestCondition, preferably by static importing the matching(org.hamcrest.Matcher<? extends T> matcher) method.
The initial example with matching books can be rewritten to use AssertJ like this:
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.HamcrestCondition.matching;
import static org.hamcrest.Matchers.*;
import static your.domain.BookMatcher.aBook();
...
Book effectiveJava = // resolve the Effective Java book
assertThat(effectiveJava).is(matching(aBook().withTitle("Effective Java").withAuthors(not(empty()))));<configuration>
<!-- default: true
can be set to false -->
<scanEnabled>true</scanEnabled>
<!-- default: ${project.groupId} -->
<scanPackages>
com.my.pkg,
com.my.other.pkg
</scanPackages>
<includes>
com.external.SomeRecord,
com.external.subpkg.SomeOtherRecord
</includes>
<excludes>
com.my.pkg.aux.RecordToExclude,
com.my.pkg.other.AnotherRecordToExclude,
</excludes>
<!-- default: pom
Maven module packaging types where you want
to skip executing the plugin -->
<skipForPackaging>pom,ear</skipForPackaging>
<!-- default: ${project.build.directory}/generated-test-sources/record-matchers -->
<outputDirectory>${project.build.directory}/custom</outputDirectory>
<!-- default: true
If you for some reason need to prevent the generated code
to be included as test sources in your project, set this to false -->
<includeGeneratedCodeAsTestSources>true</includeGeneratedCodeAsTestSources>
</configuration>The following command can also be used anywhere to view the plugin’s own description of its configuration parameters:
mvn com.github.runeflobakk:record-matcher-maven-plugin::help -Dgoal=generate -DdetailOr the short form if you have already configured the plugin in your Maven project:
mvn record-matcher:helpThe project is licensed as open source under the Apache License, Version 2.0.