The Apache Commons Lang has an interesting method called indexOf()
with the following documentation, adapted from its Javadoc:
Finds the index of the given value in the array starting at the given index. This method returns –1 for a null input array. A negative startIndex
is treated as zero. A startIndex
larger than the array length will return –1.
array
: Array to search for the object. May be null.valueToFind
: Value to find.startIndex
: Index at which to start searching.
The method returns the index of the value within the array, or –1 if not found or null.
Following is the implementation of this method.
Listing 5.8 Implementation of the indexOf
method
class ArrayUtils {
public static int indexOf(final int[] array, final int valueToFind,
➥ int startIndex) {
if (array == null) { ❶
return -1;
}
if (startIndex < 0) { ❷
startIndex = 0;
}
for (int i = startIndex; i < array.length; i++) {
if (valueToFind == array[i]) {
return i; ❸
}
}
return -1; ❹
}
}
❶ The method accepts a null array and returns -1 in such a case. Another option could be to throw an exception, but the developer decided to use a weaker pre-condition.
❷ The same goes for startIndex: if the index is negative, the method assumes it is 0.
❸ If the value is found, return the index.
❹ If the value is not in the array, return -1.
In this example, let’s first apply the techniques we already know. Start by exploring the input variables and how they interact with each other:
array
of integers:valueToFind
:startIndex
:(array,
startIndex)
:(array,
valueToFind)
:(array,
valueToFind,
startIndex)
:
We now create the test cases by combining the different partitions:
array
is nullarray
with a single element,valueToFind
inarray
array
with a single element,valueToFind
not inarray
startIndex
negative, value inarray
startIndex
outside the boundaries ofarray
array
with multiple elements,valueToFind
inarray
,startIndex
aftervalueToFind
array
with multiple elements,valueToFind
inarray
,startIndex
beforevalueToFind
array
with multiple elements,valueToFind
inarray
,startIndex
precisely atvalueToFind
array
with multiple elements,valueToFind
inarray
multiple times,startIndex
beforevalueToFind
array
with multiple elements,valueToFind
inarray
multiple times, one beforestartIndex
array
with multiple elements,valueToFind
not inarray
In JUnit, the test suite looks like the following listing.
Listing 5.9 First tests for the indexOf()
method
import static org.junit.jupiter.params.provider.Arguments.of;
public class ArrayUtilsTest {
@ParameterizedTest
@MethodSource("testCases")
void testIndexOf(int[] array, int valueToFind, int startIndex,
➥ int expectedResult) {
int result = ArrayUtils.indexOf(array, valueToFind, startIndex);
assertThat(result).isEqualTo(expectedResult);
}
static Stream<Arguments> testCases() { ❶
int[] array = new int[] { 1, 2, 3, 4, 5, 4, 6, 7 };
return Stream.of(
of(null, 1, 1, -1), ❷
of(new int[] { 1 }, 1, 0, 0), ❸
of(new int[] { 1 }, 2, 0, -1), ❹
of(array, 1, 10, -1), ❺
of(array, 2, -1, 1), ❻
of(array, 4, 6, -1), ❼
of(array, 4, 1, 3), ❽
of(array, 4, 3, 3), ❾
of(array, 4, 1, 3), ❿
of(array, 4, 4, 5), ⓫
of(array, 8, 0, -1) ⓬
);
}
}
❶ All the test cases we engineered are implemented here.
Listing 5.10 shows the test suite developed for the library method itself. I added some comments, so you can see how their tests related to our tests. This test suite contains our test cases T1, T4, T5, T6, T7, T8, T10, and T11. Interestingly, it is not testing the behavior of the array with a single element or the case in which the element appears again after the first time it is found.
Listing 5.10 Original test suite of the indexOf()
method
@Test
public void testIndexOfIntWithStartIndex() {
int[] array = null;
assertEquals(-1, ArrayUtils.indexOf(array, 0, 2)); ❶
array = new int[]{0, 1, 2, 3, 0};
assertEquals(4, ArrayUtils.indexOf(array, 0, 2)); ❷
assertEquals(-1, ArrayUtils.indexOf(array, 1, 2)); ❸
assertEquals(2, ArrayUtils.indexOf(array, 2, 2)); ❹
assertEquals(3, ArrayUtils.indexOf(array, 3, 2)); ❺
assertEquals(3, ArrayUtils.indexOf(array, 3, -1)); ❻
assertEquals(-1, ArrayUtils.indexOf(array, 99, 0)); ❼
assertEquals(-1, ArrayUtils.indexOf(array, 0, 6)); ❽
}
NOTE Parameterized tests seem to be less popular in open source systems. For methods with simple signatures, inputs, and outputs, like indexOf
, we could argue that parameterized tests are overkill. When creating this example, I considered writing two different traditional JUnit test cases: one containing only the exceptional behavior and another containing the remaining test cases. In the end, organizing test cases is a matter of personal taste—talk to your team and see what approach they prefer. We talk more about test code quality and readability.
Both test suites look good and are quite strong. But now, let’s express the main behavior of the method via property-based testing.
The overall idea of the test is to insert a random value in a random position of a random array. The indexOf()
method will look for this random value. Finally, the test will assert that the method returns an index that matches the random position where we inserted the element.
The tricky part of writing such a test is ensuring that the random value we add in the array does not already exist in the random array. If the value is already there, this may break our test. Consider a randomly generated array containing [1,
2,
3,
4]
: if we insert a random element 4 (which already exists in the array) on index 1 of the array, we will get a different response depending on whether startIndex
is 0 or 3. To avoid such confusion, we generate random values that do not exist in the randomly generated array. This is easily achievable in jqwik. The property-based test needs at least four parameters:
numbers
—A list of random integers (we generate a list, as it is much easier to add an element at a random position in a list than in an array). This list will have a size of 100 and will contain values between –1000 and 1000.value
—A random integer that is the value to be inserted into the list. We generate values ranging from 1001 to 2000, ensuring that whatever value is generated will not exist in the list.indexToAddElement
—A random integer that represents a random index for where to add this element. The index ranges from 0 to 99 (the list has size 100).startIndex
—A random integer that represents the index where we ask the method to start the search. This is also a random number ranging from 0 to 99.
With all the random values ready, the method adds the random value at the random position and calls indexOf
with the random array, the random value to search, and the random index at which to start the search. We then assert that the method returns indexToAddElement
if indexToAddElement
>= startIndex
(that is, the element was inserted after the start index) or –1 if the element was inserted before the start index. Figure 5.1 illustrates this process.
Figure 5.1 The data generation of the property-based test for the indexOf
method
The concrete implementation of the jqwik test can be found in listing 5.11.
Listing 5.11 Property-based test for the indexOf()
method
@Property
void indexOf(
@ForAll
@Size(value = 100) List<@IntRange(min = -1000, max = 1000)
➥ Integer> numbers, ❶
@ForAll
@IntRange(min = 1001, max = 2000) int value, ❷
@ForAll
@IntRange(max = 99) int indexToAddElement, ❸
@ForAll
@IntRange(max = 99) int startIndex) { ❹
numbers.add(indexToAddElement, value); ❺
int[] array = convertListToArray(numbers); ❻
int expectedIndex = indexToAddElement >= startIndex ?
indexToAddElement : -1; ❼
assertThat(ArrayUtils.indexOf(array, value, startIndex))
.isEqualTo(expectedIndex); ❽
}
private int[] convertListToArray(List<Integer> numbers) { ❾
int[] array = numbers.stream().mapToInt(x -> x).toArray();
return array;
}
❶ Generates a list with 100 numbers ranging from -1000 to 1000
❷ Generates a random number that we insert into the array. This number is outside the range of the list so we can find it easily.
❸ Randomly picks a place to put the element in the list
❹ Randomly picks a number to start the search in the array
❺ Adds the number to the list at the randomly chosen position
❻ Converts the list to an array, since this is what the method expects
❼ If we added the element after the start index, we expect the method to return the position where we inserted the element. Otherwise we expect the method to return -1.
❽ Asserts that the search for the value returns the index we expect
❾ Utility method that converts a list of integers to an array
Jqwik will generate a large number of random inputs for this method, ensuring that regardless of where the value to find is, and regardless of the chosen start index, the method will always return the expected index. Notice how this property-based test better exercises the properties of the method than the testing method we used earlier.
I hope this example shows you that writing property-based tests requires creativity. Here, we had to come up with the idea of generating a random value that is never in the list so that the indexOf
method could find it without ambiguity. We also had to be creative when doing the assertion, given that the randomly generated indexToAddElement
could be larger or smaller than the startIndex
(which would drastically change the output). Pay attention to these two points:
- Ask yourself, “Am I exercising the property as closely as possible to the real world?” If you come up with input data that will be wildly different from what you expect in the real world, it may not be a good test.
- Do all the partitions have the same likelihood of being exercised by your test? In the example, the element to be found is sometimes before and sometimes after the start index. If you write a test in which, say, 95% of the inputs have the element before the start index, you may be biasing your test too much. You want all the partitions to have the same likelihood of being exercised.In the example code, given that both
indexToAddElement
andstartIndex
are random numbers between 0 and 99, we expect about a 50-50 split between the partitions. When you are unsure about the distribution, add some debugging instructions and see what inputs or partitions your test generates or exercises.
Leave a Reply