Assertj For User Based Validation

Usually AssertJ is a pure testing framework and therefore should have test scope in our pom.xml or build.gradle. But recently i had facing a problem where my first thought was AssertJ and it was not in the test scope ;). So this article contains a PoC and this were the requirements:

  1. The system should compare the values of a pojo (only numbers) against a reference pojo with a configurable strategy. This comparison can be an exact one as well as a close to compare with a given (also configurable) offset.
  2. Thereby the implementation should collect all failing values in contrast to only the first one.
  3. And the implementation should provide a detailed failure message which means it should contain the actual and the expected value. With that users can evaluate those messages and propabliy adjust offsets for example.

As a side note: The code should be implemented as easy and readable as possible. ;)

Solution

The crucial part for me here is the AssertJ Assertions Generator which generates a PojoAssert class. This class contains hasXXX and isCloseToXXX methods which fits the requirements. Additionally i configured the plugin that way that it also generates the SoftAssertions class which collects all failures. So requirement 2 is also met...two to go

pom.xml

 1 <!-- snip -->
 2         <dependency>
 3             <groupId>org.assertj</groupId>
 4             <artifactId>assertj-core</artifactId>
 5             <version>2.0.1-SNAPSHOT</version>
 6         </dependency>
 7 <!-- snip -->
 8         <plugins>
 9             <plugin>
10                 <groupId>org.assertj</groupId>
11                 <artifactId>assertj-assertions-generator-maven-plugin</artifactId>
12                 <version>1.6.1-SNAPSHOT</version>
13                 <configuration>
14                     <classes>
15                         <param>de.bischinger.validation.model.MyPojo</param>
16                     </classes>
17 
18                     <targetDir>src/main/java</targetDir>
19                     <generateAssertions>false</generateAssertions>
20                     <generateBddAssertions>false</generateBddAssertions>
21                     <generateSoftAssertions>true</generateSoftAssertions>
22                     <generateJUnitSoftAssertions>false</generateJUnitSoftAssertions>
23                 </configuration>
24             </plugin>
25         </plugins>

Note: If you wonder about the SNAPSHOT versions, the isCloseToXXX methods are currently not in version 1.6.0 but will come soon.

If you take a close look at the generated failure messages of the AssertJ assertions you can see that every AssertionError contains the expected as well as the actual values. If you are not satisfied with that you also have the possibility to override that message to your needs. Only one requirement to go...

Failure message example with two failures

 1 The following 2 assertions failed:
 2 1) 
 3 Expecting sum2:
 4   <2>
 5 to be close to:
 6   <-5>
 7 by less than <1> but difference was <7>
 8 2) 
 9 Expecting sum4:
10   <4>
11 to be close to:
12   <10>
13 by less than <1> but difference was <6>

The configurable part can be done with a Strategy design pattern which takes the actual and expected pojo as well as indexed based offsets. This example provides one strategy for exact checks and one strategy for is close to checks where as the latter one takes use of the offsets.

 1 @FunctionalInterface
 2 public interface ValidationStrategy {
 3 
 4     void validate(MyPojo actual, MyPojo expected, long[] offset);
 5 
 6     //Offset ignored here
 7     ValidationStrategy VALIDATE_ALWAYS_EQUAL = (a, e, o) -> {
 8         SoftAssertions softAssertions = new SoftAssertions();
 9         softAssertions.assertThat(a).hasSum1(e.getSum1());
10         softAssertions.assertThat(a).hasSum2(e.getSum2());
11         softAssertions.assertThat(a).hasSum3(e.getSum3());
12         softAssertions.assertThat(a).hasSum4(e.getSum4());
13 
14         softAssertions.assertAll();
15     };
16 
17     ValidationStrategy VALIDATE_BY_OFFSETMAPPING = (a, e, o) -> {
18         SoftAssertions softAssertions = new SoftAssertions();
19         softAssertions.assertThat(a).hasCloseToSum1(e.getSum1(), o[0]);
20         softAssertions.assertThat(a).hasCloseToSum2(e.getSum2(), o[1]);
21         softAssertions.assertThat(a).hasCloseToSum3(e.getSum3(), o[2]);
22         softAssertions.assertThat(a).hasCloseToSum4(e.getSum4(), o[3]);
23 
24         softAssertions.assertAll();
25     };
26 }
 1 public class ValidationStrategyTest {
 2 
 3     private static MyPojo actual;
 4 
 5     @BeforeClass
 6     public static void before() {
 7         actual = new MyPojo(1, 2, 3, 4);
 8     }
 9 
10     @Test
11     public void should_pass_EqualsStrategy_when_everything_is_equal() throws Exception {
12         ALWAYS_EQUAL.validate(actual, actual, null);
13     }
14 
15     @Test(expected = SoftAssertionError.class)
16     public void should_fail_EqualsStrategy_with_non_equal() throws Exception {
17         ALWAYS_EQUAL.validate(actual, new MyPojo(1, 2, 0, 3), null);
18     }
19 
20     @Test
21     public void should_pass_OffsetStrategy_when_everything_is_equal() throws Exception {
22         BY_OFFSETMAPPING.validate(actual, actual, 0, 0, 0, 0);
23     }
24 
25     @Test
26     public void should_pass_OffsetStrategy_when_everything_is_closeTo() throws Exception {
27         BY_OFFSETMAPPING.validate(actual, new MyPojo(0, 3, 2, 5), 1, 1, 1, 1);
28     }
29 
30     @Test(expected = SoftAssertionError.class)
31     public void should_fail_OffsetStrategy_when_something_is_not_closeTo() throws Exception {
32         BY_OFFSETMAPPING.validate(actual, new MyPojo(0, -5, 2, 10), 1, 1, 1, 1);
33     }
34 }

If you are interested in the PoC you can find the sources on my Github-Repository.

Note: If you are using JBoss Forge try out the new JBoss Forge AssertJ addon.