A Java library for generating test values, particularly for edge cases.
A regular occurrence in testing is the need to generate values that match a specification (for example, "all valid email addresses") and also values that violate that specification. In both cases, particular attention must be paid to edge cases, or values that are at the limit (or edge) of a specification. Edgeifier helps generate these values with a simple, fluent Java API.
- Simple, fluent Java API that generates infinite streams of values
- Generates primitive, String, and date values
- Can generate custom value types
- No runtime dependencies
Edgeifier requires Java 8 or later.
Add the following dependencies to your pom.xml file:
<dependency>
<groupId>com.handcraftedbits.edgeifier</groupId>
<artifactId>edgeifier-api</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.handcraftedbits.edgeifier</groupId>
<artifactId>edgeifier-impl</artifactId>
<version>1.0.0</version>
<scope>runtime</scope>
</dependency>Then, create an Edgeifier instance:
Edgeifier edgeifier = new Edgeifier();By default, this will use the current time as a random seed. In general though, you'll want to use a specific seed
value to ensure that the same values are generated during every test run. In that case, simply pass the seed value to
the Edgeifier constructor:
Edgeifier edgeifier = new Edgeifier(123456L);You can use the Edgeifier object to declare a specification for any value type. Once you have a specification, you
can then create an infinite Stream of
values.
For example, let's say we want to test that our checkValue() function can only accept values between 0 and 100.
First, we need to make a Stream of integers matching that specification:
Stream<Integer> matching = edgeifier.makeIntsLike().any().between(0, 101).stream(); // Note that maximum value is exclusive.Also, we'll want to make a Stream of integers that violate that specification:
Stream<Integer> violating = edgeifier.makeAnyOf(
edgeifier.makeIntsLike().any().lessThan(0),
edgeifier.makeIntsLike().any().atLeast(101)).stream();Finally, we can use these Streams to test our function (assume that checkValue() returns true if the value is
within our defined range and false otherwise):
@Test
public void testCheckValue () {
matching.limit(100).forEach(value -> Assertions.assertTrue(checkValue(value)));
violating.limit(100).forEach(value -> Assertions.assertFalse(checkValue(value)));
}Note that we use Stream.limit()
to limit the number of values generated; the Streams created by Edgeifier are infinite by default. You should use a
limit that makes sense in your situation.
Consult the Javadoc for additional information on generating primitive types.
Edgefier excels in creating Strings of arbitrary complexity. For example, let's assume that we have a URL validator
that will accept any URL accepted by java.net.URL with a maximum length of 1024 characters. We want to test this
validator with a variety of URLs, so let's use the following specification:
- The scheme can be either
httporhttps - The hostname can optionally start with
www - The hostname can be between
4and20characters in length and can end with.com,.edu, or.net - The port can be between
80and9000or missing altogether - There can be between
1and4path segments with length between1and32characters
The Edgefier API makes it easy to create this specification in code. First, let's make a ValueBuilder for the port
value:
ValueBuilder<String> ports = edgeifier.makeStringsLike()
.thisOne(":")
.plus()
.builder(edgeifier.makeIntsLike().any().between(80, 9001));Notice what we're doing: concatenating the port specifier (":") with the output of another ValueBuilder (the one
generating random port values).
We'll also need to make a path segment ValueBuilder:
ValueBuilder<String> segments = edgeifier.makeStringsLike()
.thisOne("/")
.plus()
.anyInRange('a', 'z').repeat(1, 33);Now, let's combine that ValueBuilder with the one that will generate the rest of the URL:
ValueBuilder<String> urls = edgeifier.makeStringsLike()
.anyOf("http://", "https://")
.plus()
.thisOne("www.").optional()
.plus()
.anyInRange('a', 'z').repeat(4, 21)
.plus()
.anyOf(".com", ".edu", ".net")
.plus()
.builder(ports).optional()
.plus()
.builder(segments).repeat(1, 5);Notice that we can generate Strings of arbitrary complexity by combining fragments consisting of either generated
String values or the output of other ValueBuilder objects via plus().
What kind of values will this ValueBuilder generate? Let's test it out, but for sake of readability we'll use
Stream.filter() to only return URLs that are exactly 60 characters in length:
urls.stream().filter(value -> value.length() == 60).limit(5).forEach(System.out::println);Here's what we get:
http://ztctgwxyiabvfwkkn.edu/so/uqhdcmsulkmo/fxaodnssqufyhpx
https://diabvmmmoduidpf.com/gfprsxg/dgshizrcabzkkhqeluspsnpn
http://rybzux.edu/kdheasaxgqmquq/uapvgcpucntrdfdrmqiojhyzadu
https://www.owvdy.com:5446/poarcmqikpbr/xznbnrjl/osfbigcvsrt
https://www.pbjn.com:7384/psh/h/xauvauxnsxtwpwdaeu/uvzdexmze
To positively test our validator, we can simply use this ValueBuilder to generate any number of URLs up to 1024
characters in length. Keeping in mind edge cases though, we should also specifically test URLs that are exactly 1024
characters in length. To negatively test our validator, we can use this same ValueBuilder and test with URLs of
length greater than 1024 characters. We should also make a similar ValueBuilder that contains e.g., bad schemes,
invalid ports, etc. More information about generating String values can be found in the
Javadoc.
As an added convenience, Edgeifier includes a utility class to help generate characters from all of the Unicode
character classes. See the UnicodeBuilder Javadoc
for more information.
Edgeifier can be used to create collections (via java.util.List) of values of arbitrary length. For example, we could
create a ValueBuilder capable of generating lists of positive integers with lengths between 5 and 10 elements like
so:
ValueBuilder<List<Integer>> lists = edgeifier.makeListsLike()
.any(edgeifier.makeIntsLike().any().atLeast(0))
.withLengthBetween(5, 11); // Note that maximum length is exclusive.For more information, see the Javadoc.
Edgeifier can generate custom types via
CustomValueProvider
instances. As an example, let's use Edgeifier to generate random instances of the following bean:
public class TestBean {
private int value;
public TestBean (int value) {
this.value = value;
}
public int getValue () {
return this.value;
}
}First, we need to create a CustomValueProvider capable of creating TestBean instances:
public class TestBeanValueProvider implements CustomValueProvider<TestBean> {
private int max = 10;
public TestBean generateValue (Edgeifier edgeifier) {
return new TestBean(edgeifier.makeIntsLike().any().between(0, this.max).stream().findFirst().get());
}
@Override
public void setProperty (String name, Object value) {
if (name.equals("max")) {
this.max = (int) value;
}
}
}Notice the setProperty method: this is used to set arbitrary custom properties which control the generated value. In
this case, if a property named max is set, we'll use that to control the maximum value associated with the generated
TestBean instance. The generateValue method is used to create our TestBean instance. Notice that the current
Edgeifier is provided. With it, you can generate any type of value required to populate your bean. In this example we
are generating an infinite stream of integers between 0 and max and selecting the first element as the value for
TestBean.value.
Next, we need to create a
CustomValueProviderFactory
class that will help Edgeifier create instances of our CustomValueProvider:
public class TestBeanValueProviderFactory implements CustomValueProviderFactory<TestBean> {
@Override
public CustomValueProvider<TestBean> newCustomValueProvider () {
return new TestBeanValueProvider();
}
@Override
public Class<TestBean> valueClass () {
return TestBean.class;
}
}We can register this class programmatically:
edgeifier.registerCustomValueProviderFactory(new TestBeanValueProviderFactory());Or by including a file named
META-INF/services/com.handcraftedbits.edgeifier.api.value.custom.CustomValueProviderFactory containing the
fully-qualified name of our TestBeanValueProviderFactory on the classpath.
Finally, we can make use of this custom type with our Edgeifier instance:
edgeifier.makeValuesLike().any(TestBean.class).stream() // Use the default max value of 10.
edgeifier.makeValuesLike().any(TestBean.class).withProperty("max", 100).stream(); // Use a custom max value.