Skip to content

koranke/object-verifier

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ObjectVerifier

Purpose:

QA test framework for assertions in Java. To assert that an expected custom object and an actual custom object are equal or pass other comparison criteria. No special supporting code in the custom class is needed. There is no need to implement an equals method.

This framework's primary purpose is to make it easy to verify complex objects. Expected results must be constructed using an instance of the same class that is used to hold actual results. For example, a web service call returns a JSON document that is deserialized into a Java object. You can use this framework to verify the content of the deserialized object by populating another instance of the object with expected results and then using the Verify.that() method.

Normally, a test might include a series of assertions for each member of an object. For example, the object under test includes 10 different members. In this case, a test would need to include 10 different assertions. When using this framework, the test only needs to make one assertion. Of course, it still needs to specify each of the 10 expected result values by assigning them to a companion "expected result" object.

For example, an API under test is for a UserService that returns details on a User. A test makes a call to the service and verifies that the return data is correct. Typically, this would look as follows.

@Test
public void testCanGetUserDetails() {
	...
	User user = UserServiceClient.getUser(userId);
	Assert.assertEquals(user.getId(), userId, "Invalid user Id.");
	Assert.assertEquals(user.getFirstName(), "John", "Invalid first name.");
	Assert.assertEquals(user.getLastName(), "Doe", "Invalid last name.");
	Assert.assertEquals(user.getAge(), 20, "Invalid age.");
	Assert.assertEquals(user.getGender(), "m", "Invalid gender.");
	Assert.assertEquals(user.getIsMarried(), false, "Invalid marriage status.");
	Assert.assertEquals(user.getDateAccountCreated(), LocalDate.now.toString() , "Invalid account creation date.");
	Assert.assertEquals(user.getAddress().getStreet(), "123 Main St", "Invalid address street.");
	Assert.assertEquals(user.getAddress().getCity(), "Billton", "Invalid city.");
}

Using the framework, if expected results were manually specified, it would look something like this.

@Test
public void testCanGetUserDetails() {
	...
	User user = UserServiceClient.getUser(userId);
	User expectedUser = new User().setId(userId)
		.setFirstName("John")
		.setLastName("Doe")
		.setAge(20)
		.setGender("m")
		.setIsMarried(false)
		.setDateAccountCreated(LocalDate.now.toString());
	expectedUser.getAddress().setStreet("123 Main St").setCity("Billton");
	
	Verify.that(user).isEqualTo(expectedUser);
}

Alternatively, if the expected results can be determined from an alternative source of truth (for example, a database or other business logic), then a custom method can be created to populate expected results rather than manually populating expected values.

@Test
public void testCanGetUserDetails() {
	...
	User user = UserServiceClient.getUser(userId);
	User expectedUser = getExpectedUser(userId);
	Verify.that(user).isEqualTo(expectedUser);
}

Usage

Basic assertion:

Verify.that(actualTrack).isEqualTo(expectedTrack);

There are two supporting classes that are needed for more complex object comparisons: FieldsToCheck and VerificationRule. FieldsToCheck lets you control which fields to check in an object and VerificationRule lets you control how to compare two objects. If you do not specify a FieldsToCheck object, all fields will be checked. If you do not specify any VerificationRules, all fields will be checked for exact matches.

Use Cases:

Verify all

If all object members need to be checked and the default verification rules are what are needed, there is no need to specify fields to check or verification rules.

Verify.that(actualTrack).isEqualTo(expectedTrack)

Verify only two fields

If only two members of an object need to be tested, the fields to check must be specified.

FieldsToCheck fieldsToCheck = new FieldsToCheck()
				.withKey(Track.class)
				.includeField("trackId")
				.includeField("trackTitle");

Verify.that(actualTrack).usingFields(fieldsToCheck).isEqualTo(expectedTrack)

Verify all fields except for one

If all fields except one need to be tested, specify the field to exclude.

FieldsToCheck fieldsToCheck = new FieldsToCheck().
				withKey(Track.class)
				.excludeField("trackId");

Verify.that(actualTrack).usingFields(fieldsToCheck).isEqualTo(expectedTrack)

Do not check for exact order when comparing fields that are collections

If the object under test includes one or more members that are collections, and if the order of those collections is not important, use the ListUnsortedRule.

Verify.that(actualTrack).usingRule(new ListUnsortedRule()).isEqualTo(expectedTrack);

Consider dates as equal if they are within 5 minutes of each other

If the object under test includes a date value, it's unlikely that the actual time and the expected time will precisely match. If that is the case, use the DateTimeInRange rule. In the below example, if the actual date is withing 5 minutes (5 more or 5 less) of the expected date, the test will pass.

Verify.that(actualTrack)
		.usingRule(new DateTimeInRangeRule(5, ChronoUnit.MINUTES))
		.isEqualTo(expectedTrack);

Use a comparison rule for a specific field

Verification rules can be global, in which case they apply to all fields for all objects, or they can be field specific. Rules specified with "usingRule" are global. To apply a rule to a field, pass the rule in as a parameter for "includeField". In the below example, "artistNames" and "keyWords" are lists. "artistNames" will be verified using the default rule for exact match, but "keyWords" will be verified using the ListUnsortedRule.

FieldsToCheck fieldsToCheck = new FieldsToCheck()
				.withKey(Track.class)
				.includeField("trackName")
				.includeField("artistNames")
				.includeField("keyWords", new ListUnsortedRule());

Verify.that(actualTrack)
		.usingFields(fieldsToCheck)
		.isEqualTo(expectedTrack);

Check all fields but don't check for exact order when comparing one collection

If you need to specify a field-specific rule but also need to verify all fields, use the method withFieldRule() instead of includeField(). Otherwise, only the fields specified by "includeField()" will be checked.

FieldsToCheck fieldsToCheck = new FieldsToCheck()
				.withKey(Track.class)
				.withFieldRule("keyWords", new ListUnsortedRule());

Verify.that(actualTrack)
		.usingFields(fieldsToCheck)
		.isEqualTo(expectedTrack);

Child objects: Checking all fields and using default validation for all

If the object under test has a member that itself is a custom object, the child object will also be verified. For example, if the Track object includes a list of related Track objects, the following call will verify both the parent Track object and the child track objects.

Verify.that(actualTrack).isEqualTo(expectedTrack);

Child objects: check all fields for the parent except one. Only check one field for a child object

In this case, for the parent object, all members except one need to be checked. For the child object, only one field needs to be checked.

FieldsToCheck fieldsToCheck = new FieldsToCheck()
				.withKey(Album.class)
				.excludeField("id")
				.withKey(Track.class)
				.includeField("id");

Verify.that(actualTrack)
		.usingFields(fieldsToCheck)
		.isEqualTo(expectedTrack);

Compare multiple pairs of objects and provide a context message in case a comparison fails

When looping through more than one pair of objects for comparison, if there is a failure, there needs to be a context message to help understand at which point the failure occurred.
For example, there is a list of 50 users ids that need to be tested. The test will call the service under test to get the user details and will also populate a companion "expected" user based on values stored in a database.
With no context message the test could fail with something like "User.Age equality assertion failed. Actual Age: 12 Expected Age: 14". Of course, you'd have no idea which user out of the 50 had this problem. To avoid this issue, specify a context message.

...
for (Long testId : testIds) {
	User actualUser = UserServiceClient.getUser(testId);
	User expectedUser = UserDataStore.getExpectedUser(testId);
	Verify.that(actualUser)
		.withContextMessage(String.format("Checking actual user %d.", testId))
		.isEqualTo(expectedUser);
}
...

Now if there is a failure, the context message will report on exactly which user the failure occurred.

Details:

Supported data types

Verify.that() can test the following.

  • Two domain objects
  • Two lists of domain objects
  • Two arrays of domain objects

Within a custom object, the following can be tested:

  • Native data types: boolean, byte, short, int, long, double, float, char
  • Simple data types: String, Boolean, Byte, Short, Integer, Long, Double, Float, Char
  • Dates/Time: Calendar, Date, Timestamp, LocalDate, LocalDateTime
  • Other custom objects
  • Lists of any data type
  • Arrays of any data type
  • Sets of any data type
  • Maps of any data type
  • Any object that implements the equals method

FieldsToCheck

FieldsToCheck allows you to specify which fields to check and it also allows you to associate verification rules at the field level. FieldsToCheck allows you to configure fields for both a parent object and any child objects. This is done by setting the current key to the desired custom object and then adding or excluding fields as needed.

For example:

FieldsToCheck fieldsToCheck = new FieldsToCheck()
				.withKey(Album.class)
				.excludeField("id")
				.withKey(Track.class)
				.includeField("id");

In the above example, all fields in the Album object will be checked except for the "id" field. The Album object contains a list of child Track objects. Only the "id" field will be checked for the child Track objects.

In cases where you need to specify a field-level VerificationRule but also need to check all fields, use the method withFieldRule() instead of includeField().

For example:

FieldsToCheck fieldsToCheck = new FieldsToCheck()
				.withKey(Album.class)
				.withFieldRule("releaseDate", new DateTimeInRangeRule(5, ChronoUnit.MINUTES))
				.withKey(Track.class)
				.includeField("id");

In the above example, all fields for Album will be checked, but only the "id" field will be checked for Track objects.

You do not have to specify a key. Omitting the "key" statement will result in all fields matching the specified name to be included or excluded. For example, the object under test has a member named "id". It also has a child object and that object also has a member named "id". The following code would result in the "id" field for both the parent and child getting checked. If the object under test has no child objects or if the member names that need to be checked are unique across both parent and child objects, you can simplify the statement by omitting the key specifiers.

FieldsToCheck fieldsToCheck = new FieldsToCheck()
				.includeField("id");

Finally, there is a shortcut for adding fields. If you don't need to specify the class key for the field, you can simply pass in the field names as string values. In this case, verification will run on any field that matches the name, regardless of the class that the field name is associated with.

For example:

Verify.that(actualTrack).usingFields("id", "releaseDate").isEqualTo(expectedTrack);

Verification will fail if you misspell an object field name. For example, if the object under test included a field named "title" but we tried to verify a field named "tittle", verification would fail with a message that the field "tittle" could not be found.

Verify.that(actualTrack).usingFields("tittle").isEqualTo(expectedTrack);

transient keyword

If you have a class where you want to block certain fields from ever getting compared, you can use the Java "transient" keyword for the field declaration. For example.

private transient String fieldToIgnore;

Verification Rules

A verification rule controls how two items are compared. For example, the StringContainsRule will assert that the actual result contains the expected result.

Verification rules can be applied globally or locally for a particular field.
Field-level rules take precedence over global rules. Verification rules contain application rules that control which data types the rule is applied to. For example, the StringExactMatchRule contains a StringApplicationRule, consequently this verification rule only applies to items of type String.

When evaluating global verification rules and field-level rules, global rules will not be used in any case where a field-level rule uses the same application rule. For example, if there is a global rule for StringExactMatchRule and a field-level rule for StringContainsRule, since both use the same StringApplicationRule, only the field-level rule will be used (for that particular field).

For simple equality assertions, there is no need to specify verification rules. However, for things like case-insensitive string comparisons, in-range comparisons, etc, verification rules are needed. Some verification rules can take one or more parameters (DateTimeInRangeRule, StringExactMatchRule) whereas others do not take parameters (ListUnsortedRule, MapContainsRule).

List of Verification Rules With Examples

  • DateTimeComparisonRule(ComparisonType)

In this example, if expected time is 4:30, then assertion will pass if the actual time is after 4:30.

new DateTimeComparisonRule(DateTimeComparisonRule.ComparisonType.after)
  • DateTimeInRangeRule(long range, TemporalUnit timeUnit)

In this example, if expected time is 4:30, then assertion will pass if the actual time is between 3:30 and 5:30.

new DateTimeInRangeRule(1, ChronoUnit.HOURS)
  • ListContainsRule()

In this example, if expected list is 1, 2, 3, then assertion will pass if the actual list contains all of the items in the expected list, regardless of any extra items in the actual list that are not contained in the expected list.

new ListContainsRule()
  • ListDoesNotContainsRule()

In this example, if expected list is 1, 2, 3, then assertion will pass if the actual list does not contain any of the items in the expected list.

new ListDoesNotContainsRule()
  • ListExactMatchRule()

In this example, if expected list is 1, 2, 3, then assertion will pass only if the actual list contains the exact same items in the exact same order.

new ListExactMatchRule()
  • ListUnsortedRule()

In this example, if expected list is 1, 2, 3, then assertion will pass if the actual list contains the exact same items, regardless of the order.

new ListUnsortedRule()
  • MapContainsRule()

In this example, if expected map contains the keys 1 and 2 with values "one" and "two", then assertion will pass if the actual map contains all of the keys in the expected map, regardless of any extra items in the actual list that are not contained in the expected list, and that the values of those keys also match.

new MapContainsRule()
  • MapExactMatchRule()

In this example, if expected map contains the keys 1 and 2 with values "one" and "two", then assertion will pass only if the actual map contains the exact same keys in the expected map and that the values of those keys also match.

new MapExactMatchRule()
  • NumberInRangeRule(long range)

In this example, if expected number is 465, then assertion will pass if the actual number is between 455 and 475.

new NumberInRangeRule(10)
  • NumberMatchRule(NumericComparison numericComparison)

In this example, if expected number is 8, then assertion will pass if the actual number is less than or equal to 8.

new NumberMatchRule(NumericComparison.lessThanOrEqualTo)
  • StringContainsRule(CaseComparison caseComparison)

In this example, if expected string is "cat", then assertion will pass if the actual string contains "cat", regardless of case.
For example, "CATALOG" or "Calcat".

new StringContainsRule(CaseComparison.caseInsensitive)
  • StringExactMatchRule(CaseComparison caseComparison)

In this example, if expected string is "Cat", then assertion will pass only if the actual string is also "Cat".

new StringExactMatchRule(CaseComparison.caseSensitive)

Notes

  • If both an actual and expected item are null, the comparison will pass.
  • If only one of the compared items is null, the comparison will fail.

Limitations

  • Getter methods for a domain object must follow the pattern "get[Fieldname]". For example, the getter for field "age" must be "getAge". This can be a problem for boolean fields where the getter may have a different pattern. For example, if the field is "isEnabled", the getter might be named "getEnabled". In this case, verification will not find the field and it won't be included in verification. Instead, the field getter should be named "getIsEnabled".
  • Value comparisons are achieved through Introspection. This is slower than getting values directly through an object. This is not significant when comparing objects that have collections with multiple items in the 10s or even 100s, but the time impact can be significant if the objects under comparison have collections with thousands of items.

About

Java Test Framework for asserting equality between two different complex objects or collections of objects.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages