This utility library helps to assert complex models using various settings and provide convenient error messages using AssertJ SoftAssertion class.
To add library to your project perform next steps:
Add the following dependency to your pom.xml:
<dependency>
<groupId>com.github.vladislavsevruk</groupId>
<artifactId>recursive-assertion-assertj</artifactId>
<version>1.0.0</version>
</dependency>
Add the following dependency to your build.gradle:
implementation 'com.github.vladislavsevruk:recursive-assertion-assertj:1.0.0'
Let's assume that we have following model classes:
public class User {
private Long id;
private Contacts contacts;
private List<Order> orders;
// getters and setters
...
}
public class Contacts {
private String email;
private String phoneNumber;
// getters and setters
...
}
public class Order {
private Long id;
private Boolean isDelivered;
// getters and setters
...
}
To compare two models all you need to do is to call
User actual = getActualUser(); // some method for receiving actual value
User expected = getExpectedUser(); // some method for preparing expected value
RecursiveAssertion.assertThat(actual).isEqualTo(expected);
You can use SoftAssertions to perform several RecursiveAssertions or along with another assertions:
User actual = getActualUser(); // some method for receiving actual value
User expected = getExpectedUser(); // some method for preparing expected value
SoftAssertions softAssertions = new SoftAssertions();
... // any additional assertions for softAssertions
RecursiveAssertion.assertThat(actual).useSoftAssertions(softAssertions).isEqualTo(expected);
// do not forget to call 'assertAll' method at the end
softAssertions.assertAll();
Some of expected values cannot be predicted in advance (e.g. automatically generated id of new database item during parallel test run) but we don't want our object verification to fail because of some of fields at our expected model weren't set. You can ignore verifications for fields that have null value at expected model using ignoreNullFields method (default value is false):
User actual = new User();
actual.setContacts(new Contacts());
User expected = new User();
// verification won't be failed because of 'contacts' field has 'null' value at expected model
RecursiveAssertion.assertThat(actual).ignoreNullFields(true).isEqualTo(expected);
// verification will be failed
RecursiveAssertion.assertThat(actual).ignoreNullFields(false).isEqualTo(expected);
expected.setOrders(Collections.emptyList());
// verification will be failed because of 'orders' field has 'null' value at actual model
RecursiveAssertion.assertThat(actual).ignoreNullFields(true).isEqualTo(expected);
You can skip verifications of fields with specific name using ignoreFields method:
User actual = new User();
actual.setId(1L);
Order actualOrder = new Order();
actualOrder.setId(2L);
actual.setOrders(Collections.singletonList(actualOrder));
User expected = new User();
Order expectedOrder = new Order();
expectedOrder.setId(3L);
expected.setOrders(Collections.singletonList(expectedOrder));
// verification won't be failed as 'id' fields validation
// will be skipped for both 'User' and 'Order' models
RecursiveAssertion.assertThat(actual).ignoreFields("id").isEqualTo(expected);
If you want to skip verification for field with specific name but your model have nested object with field that have same name which you still want to verify you can ignore field by specific path using ignoreFieldsByPath method:
User actual = new User();
actual.setId(1L);
User expected = new User();
// verification won't be failed
RecursiveAssertion.assertThat(actual).ignoreFieldsByPath("User.id").isEqualTo(expected);
Order actualOrder = new Order();
actualOrder.setId(2L);
actual.setOrders(Collections.singletonList(actualOrder));
expected.setId(1L);
Order expectedOrder = new Order();
expectedOrder.setId(3L);
expected.setOrders(Collections.singletonList(expectedOrder));
// verification won't be failed
RecursiveAssertion.assertThat(actual).ignoreFieldsByPath("User.orders.id").isEqualTo(expected);
// this verification will skip 'id' validation field only for 'order' with index '0'
RecursiveAssertion.assertThat(actual).ignoreFieldsByPath("User.orders[0].id").isEqualTo(expected);
expected.setId(2L);
// verification will be failed because validation of 'id' field will be performed for 'User' model
RecursiveAssertion.assertThat(actual).ignoreFieldsByPath("User.orders.id").isEqualTo(expected);
In some cases it can be fine if actual model have one of null or empty collection values so you can specify that verification should treat both variants as one using emptyCollectionEqualNull method (default value is false):
User actual = new User();
actual.setOrders(Collections.emptyList());
User expected = new User();
// verification won't be failed
RecursiveAssertion.assertThat(actual).emptyCollectionEqualNull(true).isEqualTo(expected);
// verification will be failed
RecursiveAssertion.assertThat(actual).emptyCollectionEqualNull(false).isEqualTo(expected);
Working with collections sometimes you cannot guarantee order in which elements were received so you can sort collections before verification using sortCollections method (default value is false):
User actual = new User();
Order actualOrder1 = new Order();
actualOrder1.setId(1L);
Order actualOrder2 = new Order();
actualOrder2.setId(2L);
actual.setOrders(Arrays.asList(actualOrder1, actualOrder2));
User expected = new User();
Order expectedOrder1 = new Order();
expectedOrder1.setId(1L);
Order expectedOrder2 = new Order();
expectedOrder2.setId(2L);
// reversed order
expected.setOrders(Arrays.asList(expectedOrder2, expectedOrder1));
// verification won't be failed
RecursiveAssertion.assertThat(actual).sortCollections(true).isEqualTo(expected);
// verification will be failed
RecursiveAssertion.assertThat(actual).sortCollections(false).isEqualTo(expected);
NOTE: all collections for both actual and expected models will be sorted.
Please read Set custom comparator for class section for more details.
While verifying arrays and iterables sometimes it make sense not to perform elements verifications if actual elements number doesn't equal to expected elements number (but there are cases when such verifications are still should be performed). For picking behavior that should be applied to your verification you can use breakOnSizeInequality method (default value is true):
List<String> actual = Arrays.asList("value1", "value2");
List<String> expected = Arrays.asList("value1", "value2", "value3");
// error message will only reflect inequality of list element numbers
RecursiveAssertion.assertThat(actual).breakOnSizeInequality(true).isEqualTo(expected);
// error message will reflect inequality of list element numbers and that specific element is missed
RecursiveAssertion.assertThat(actual).breakOnSizeInequality(false).isEqualTo(expected);
While verifying arrays and iterables you can break verification of element if specified as id field value of expected model doesn't equal to value of actual model to not verify all fields of mismatched models. For this purpose you can use breakOnIdInequality method (default value is true):
// add identifier field
AssertionContextManager.getContext().getIdentifierFieldStorage()
.add(Order.class, Order.class.getDeclaredField("id"));
User actual = new User();
Order actualOrder = new Order();
actualOrder.setId(1L);
actualOrder.setIsDelivered(true);
actual.setOrders(Collections.singletonList(actualOrder));
User expected = new User();
Order expectedOrder = new Order();
expectedOrder.setId(2L);
expectedOrder.setIsDelivered(false);
expected.setOrders(Collections.singletonList(expectedOrder));
// Error message:
// 1) [User.orders[id=2].id] Expecting: <1L> to be equal to: <2L> but was not.
RecursiveAssertion.assertThat(actual).breakOnIdInequality(true).isEqualTo(expected);
// Error message:
// 1) [User.orders[id=2].id] Expecting: <1L> to be equal to: <2L> but was not.
// 2) [User.orders[id=2].isDelivered] Expecting: <true> to be equal to: <false>
RecursiveAssertion.assertThat(actual).breakOnIdInequality(false).isEqualTo(expected);
Please read Set custom identifier field for class section for more details.
By default, model class name is used for error messages and field paths generation but you can set custom model name for that purposes using as method:
User actual = new User();
actual.setId(1L);
User expected = new User();
// Error message: [User.id] Expecting: <1L> to be equal to: <null> but was not.
RecursiveAssertion.assertThat(actual).isEqualTo(expected);
// Error message: [currentUser.id] Expecting: <1L> to be equal to: <null> but was not.
RecursiveAssertion.assertThat(actual).as("currentUser").isEqualTo(expected);
NOTE: overridden name should be used for ignoreFieldsByPath method patterns.
You can add your own verifier to customize verification of any element. Simply implement
FieldVerifier
interface and add it to
FieldVerifierStorage
from context (you can reach it calling AssertionContextManager.getContext().getFieldVerifierStorage()
).
You can specify identifier field for specific class model. That field will be used as identifier for
break on id inequality feature and field path generation for arrays and collections instead
of element index for specified model class and its descendants. For that you need to add identifier field to
IdentifierFieldStorage
from context (you can reach it calling AssertionContextManager.getContext().getIdentifierFieldStorage()
). By
default, model doesn't have any identifier field.
You can set specific comparator for any model class. Such comparator will be used for specified model class and its
descendants. All you need to do is to add your Comparator to
ComparatorStorage
from context (you can reach it calling AssertionContextManager.getContext().getComparatorStorage()
). For models
without specified custom comparator default hash code comparator will be used.
This project is licensed under the MIT License, you can read the full text here.