Skip to content

fslev/jtest-utils

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JTest Utils Stand With Ukraine

Maven Central Build status Coverage Status

A set of testing utilities for Java

Brief

JTest-Utils contains a set of utility classes which bump up your test framework by adding some powerful features:

Real world examples

Maven Central

<dependency>
  <groupId>io.github.fslev</groupId>
  <artifactId>jtest-utils</artifactId>
  <version>${latest.version}</version>
</dependency>

Gradle: compile("io.github.fslev:jtest-utils:${latest.version}")

1. Matching

  • Match JSONs
  • Match XMLs
  • Match texts
  • Match Objects
  • Match HTTP responses

... with specific matching conditions, regular expression data capture and polling support

1.1 Match JSONs

Based on json-compare

Example:

String expected = "{\n" +
        "  \"copper\": [\n" +
        "    {\n" +
        "      \"beneath\": \"heard\",\n" +
        "      \"jack\": false,\n" +
        "      \"men\": -1365455482\n" +
        "    },\n" +
        "    \"equipment\",\n" +
        "    false\n" +
        "  ],\n" +
        "  \"speak\": -263355062.75097084,\n" +
        "  \"basis\": 1670107599\n" +
        "}";
String actual = "{\n" +
        "  \"copper\": [\n" +
        "    {\n" +
        "      \"beneath\": \"heard\",\n" +
        "      \"men\": -1365455482\n" +
        "    },\n" +
        "    \"equipment\",\n" +
        "    false\n" +
        "  ],\n" +
        "  \"speak\": -263355062.750,\n" +
        "  \"nr1\": 62.750,\n" +
        "  \"nr2\": 60.750\n" +
        "}";
ObjectMatcher.matchJson("Seems that JSONs do not match", expected, actual,
        MatchCondition.JSON_NON_EXTENSIBLE_OBJECT, MatchCondition.JSON_STRICT_ORDER_ARRAY); // matching fails
==>

org.opentest4j.AssertionFailedError: FOUND 4 DIFFERENCE(S):

_________________________DIFF__________________________
copper -> JSON ARRAY elements differ at position 1:
{
  "beneath" : "heard",
  "jack" : false,
  "men" : -1365455482
}
________diffs________
Field 'jack' was NOT FOUND

_________________________DIFF__________________________
speak -> 
Expected value: -263355062.75097084 But got: -263355062.750

_________________________DIFF__________________________
Field 'basis' was NOT FOUND

_________________________DIFF__________________________
Actual JSON OBJECT has extra fields

Seems that JSONs do not match
JSONs do not match

1.2 Match XMLs

Based on xmlunit

Example:

String expected = "<a id=\"1\"> <lorem>ipsum</lorem> </a>";
String actual = "<a id=\"2\"> <lorem>ipsum</lorem> </a>";
ObjectMatcher.matchXml("Seems that XMLs do not match",
        expected, actual, MatchCondition.XML_CHILD_NODELIST_LENGTH); // matching fails
==> 

java.lang.AssertionError: Seems that XMLs do not match
XMLs do not match

Matching is by default done using regular expressions.
If expected object contains any unintentional regexes, then quote them between \Q and \E delimiters.


Expected: Expected attribute name '/a[1]/@id' - comparing <a...> at /a[1]/@id to <a...> at /a[1]:
<a id="1">
  <lorem>ipsum</lorem>
</a>
     but: result was: 
<a id="2">
  <lorem>ipsum</lorem>
</a>

1.3 Match texts

Match texts with regex support:

String expected = "lo.*sum \\Q(test)\\E";
String actual = "lorem \n ipsum (test)";
ObjectMatcher.matchString("Texts do not match", expected, actual); // successful matching
ObjectMatcher.matchString("Texts do match, actually", expected, actual, MatchCondition.DO_NOT_MATCH); // matching fails

-->
org.opentest4j.AssertionFailedError: Texts do match, actually

Strings match!

1.4 Match Objects

While matching any two Objects using ObjectMatcher.match(), one of the matching mechanisms from above will be applied in this order:

if Objects can be converted to JSON, then match as JSONs

else, if Objects are XML strings, then match as XMLs

else, match objects as texts

Example:

String expected = "{\"a\":1}";
String actual = "{\"a\":1}";
ObjectMatcher.match("Objects were converted and matched as JSONs", expected, actual); // successful matching

expected = "<a>1</a>";
actual = "<a>1</a>";
ObjectMatcher.match("Objects were converted and matched as XMLs", expected, actual); // successful matching

expected = "{\"a\":i am not a json}";
actual = "{\"a\":i am not a json}";
ObjectMatcher.match("Objects were matched as texts", expected, actual); // successful matching

1.5 Match HTTP responses

If your test framework is querying REST services and makes assertions on the response data, then you might find this type of matching very useful.
Example:

String expected = "{\"status\": 200, \"headers\":[{\"Content-Length\":\"157\"}], \"body\":{\"employee\":\"John Johnson\"}}";
String actual = "{\"status\": 200, \"headers\":[{\"Content-Length\":\"157\"}], \"body\":{\"employee\":\"John Johnny\"}}";
ObjectMatcher.matchHttpResponse("Matching failure", from(expected), from(actual)); // matching fails

-->
org.opentest4j.AssertionFailedError: FOUND 1 DIFFERENCE(S):


_________________________DIFF__________________________
employee -> 
Expected value: "John Johnson" But got: "John Johnny"

HTTP Response bodies do not match!
Matching failure

JSONs do not match

First off, this is a show purpose example, where the actual HTTP response is represented as text, converted to io.jtest.utils.matcher.http.PlainHttpResponse and passed to the ObjectMatcher.matchHttpResponse() method.
Normally, the actual object is a real HTTP response received from server and has many implementations depending on the HTTP client you are using. For this, you have to build the PlainHttpResponse object yourself, by using its builder: PlainHttpResponse.Builder.create().
The expected object is usually represented as text and can be simply converted to io.jtest.utils.matcher.http.PlainHttpResponse by using any library with object deserialization support:

Example:

import io.jtest.utils.matcher.http.PlainHttpResponse;
import com.fasterxml.jackson.databind.ObjectMapper;

public static PlainHttpResponse from(String content) {
    try {
        return new ObjectMapper().readValue(content, PlainHttpResponse.class);
    } catch (JsonProcessingException e) {
        throw new PlainHttpResponse.ParseException("Cannot parse content", e);
    }
}

The beautiful part while comparing HTTP responses is the fact that depending on the type of the response body, one of the matching mechanisms from above will be automatically applied.
In other words, the HTTP response bodies / entities might be matched as JSONs, XMLs or texts.
HTTP statuses and reasons are compared as texts while HTTP headers as JSONs.

1.6 Match with Polling support (Deprecated)

All the matching mechanisms from above support polling.
The most used case is when we need to compare HTTP responses from a service which delivers the desired data asynchronously.
In this case we need to retry the execution of client request until the actual response is matched.

String expected = "{\"status\": 200, \"body\":{\"employee\":\"John Johnson\"}}";
ObjectMatcher.matchHttpResponse("Result not found", from(expected),
        () -> from(client.execute()),
        Duration.ofSeconds(30), 1000L);

1.7 Match and Capture

Only comparing objects is not enough in the real world of testing. Often, we need to parse the results that we've just matched, extract specific data and use it inside the next test step. Writing code for this, repeatedly, can be cumbersome.
So, in case of successful matching, we can extract the data we need by using custom placeholders delimited by
~[ and ] inside the expected object:

String expected = "{\n" +
        "  \"copper\": [\n" +
        "    {\n" +
        "      \"beneath\": \"~[someValueForBeneath]\"\n" +
        "    }\n" +
        "  ],\n" +
        "  \"speak\": \"~[speakValue]\"\n" +
        "}";
String actual = "{\n" +
        "  \"copper\": [\n" +
        "    {\n" +
        "      \"beneath\": \"heard\",\n" +
        "      \"men\": -1365455482\n" +
        "    }\n" +
        "  ],\n" +
        "  \"speak\": -263355062.750,\n" +
        "  \"nr2\": 60.750\n" +
        "}";
        
Map<String, Object> capturedData = ObjectMatcher.matchJson(null, expected, actual);

assertEquals("heard", capturedData.get("someValueForBeneath"));
assertEquals("-263355062.750", capturedData.get("speakValue"));

This feature is available for any flavour of object matching.

2. Polling (Deprecated)

Retry an operation until desired result or timeout is reached:

Integer result = new Polling<Integer>()
        .duration(Duration.ofSeconds(30), 5L)
        .exponentialBackOff(1.0)
        .supplier(() -> generateRandomFromInterval(4, 7))
        .until(number -> number == 6).get();
        
assertEquals(6, result);

3. Resource reader

Read resource as String from either classpath or absolute path:

ResourceUtils.read("features/file.txt") // relative to classpath
ResourceUtils.read("/opt/project/hello/src/test/resources/features/file.txt") // absolute path

Recursively read all files from a directory, using extension filter:

Map<String, String> data = ResourceUtils.readDirectory("features/data", ".properties", ".txt", ".html", ".json");

assertEquals("lorem content", data.get("features/data/foo.txt"));
assertEquals("<html>text</html>", data.get("features/data/bar.html"));
assertEquals("{\"elephants\" : 1}", data.get("features/data/zoo/animals.json"));

4. Real world examples

Explore cucumber-jutils-tutorial to discover how most of these features are used in real life test scenarios.

Website

https://fslev.github.io/jtest-utils/