Skip to content
mutastic

Java edition

Streams

Operators that change a stream pipeline: a stage left out, or one kind of match for another. Together they ask whether your tests’ input is input each stage has to change.

OperatorChangesSet
stream/remove-stagea stream stage, removeddefaults
stream/swap-matchanyMatch, allMatch and noneMatch, swappeddefaults
stream/remove-other-stagepeek, and map on a primitive stream, removedextended

Each example is a class in samples/operator-docs with a test that misses its mutant, and each card is what mutastic run samples/operator-docs --operators=all prints for it.

stream/remove-stage, in the defaults. A stage of a stream pipeline is removed, and the stream goes on without it: filter, distinct, sorted, limit, skip, dropWhile or takeWhile, on Stream, IntStream, LongStream and DoubleStream.

The bug it stands for: a stage that’s wrong, or in the wrong place, and no test would know: a filter with the wrong condition, a distinct() someone forgot, a limit that’s off by one, a sort by the wrong key. Streams are where business rules hide in one line: only the paid orders, the first ten results, each customer once. If removing the stage changes nothing a test checks, the rule isn’t tested at all, however many tests run the line.

/// The total of the orders that are paid.
public static int paidTotal(List<Order> orders) {
return orders.stream().filter(Order::paid).mapToInt(Order::total).sum();
}

The test has only a paid order, so the filter has nothing to drop:

@Test
void aPaidOrder() {
assertEquals(10, Orders.paidTotal(List.of(new Order(10, true))));
}
Terminal window
✗ MISSED no test needs .filter(…) in paidTotal()
┌─ src/main/java/com/example/streams/Orders.java:10
│
9 │ public static int paidTotal(List<Order> orders) {
10 │ return orders.stream().filter(Order::paid).mapToInt(Order::total).sum();
│ ^^^^^^ stage removed
11 │ }
│
= mutant - return orders.stream().filter(Order::paid).mapToInt(Order::total).sum();
+ return orders.stream().mapToInt(Order::total).sum();
= note: covered by OrdersTest#aPaidOrder (its 1 covering test still passes)
= help: add a test with an element the filter must drop, and assert it's gone
done when mutastic verify m6d3805c8 samples/operator-docs passes
reproduce mutastic run --only=m6d3805c8 samples/operator-docs
or ignore mutastic ignore m6d3805c8 --reason="<why>" samples/operator-docs

The test that catches it: one whose input the stage has to change: an unpaid order the filter must drop, a duplicate for distinct(), more elements than the limit, input out of order for sorted().

@Test
void unpaidOrdersDontCount() {
assertEquals(10, Orders.paidTotal(List.of(new Order(10, true), new Order(5, false))));
}

Edition notes: removing sorted() before a result that ignores order (count(), anyMatch, min, sum and the like) can’t change anything, so it’s skipped as equivalent; so is removing distinct() before a result that ignores duplicates. Removing limit from a stream the same method starts with Stream.iterate(seed, next) or generate, or their IntStream, LongStream and DoubleStream forms, or from ints(), longs() or doubles() given no size on Random, SecureRandom, ThreadLocalRandom, SplittableRandom or RandomGenerator, is skipped as unviable when nothing else ends the stream: the tests would hang, and a hang counted as a catch shows nothing. The iterate that takes a condition can end, and so can ints(5) and a stream with takeWhile, another limit or a findFirst or the like, so those keep their mutant. The Rust edition has this operator too, stream/remove-stage, for an iterator’s filter, skip, take, skip_while, take_while, step_by and rev.

stream/swap-match, in the defaults. anyMatch, allMatch and noneMatch swap places: anyMatch becomes allMatch and noneMatch, and each of the others becomes anyMatch.

The bug it stands for: “any” where “all” was meant, or the other way round. They read alike, and with the inputs tests usually pick, all of them matching or none, they give the same answer. It costs in checks over collections: whether any item is out of stock, whether all approvers have signed, whether none of the lines is negative. Getting it wrong lets an order through with one bad line, or blocks one with a single good one.

/// Whether any answer on a form is left blank.
public static boolean anyBlank(List<String> answers) {
return answers.stream().anyMatch(String::isBlank);
}

The test has a form with every answer blank, and one with none:

@Test
void aBlankAnswer() {
assertTrue(Forms.anyBlank(List.of(" ")));
assertFalse(Forms.anyBlank(List.of("yes")));
}
Terminal window
✗ MISSED no test tells anyMatch from allMatch in anyBlank()
┌─ src/main/java/com/example/streams/Forms.java:8
│
7 │ public static boolean anyBlank(List<String> answers) {
8 │ return answers.stream().anyMatch(String::isBlank);
│ ^^^^^^^^ anyMatch changed to allMatch
9 │ }
│
= mutant - return answers.stream().anyMatch(String::isBlank);
+ return answers.stream().allMatch(String::isBlank);
= note: covered by FormsTest#aBlankAnswer (its 1 covering test still passes)
= help: use input where some but not all elements match, and assert the result
done when mutastic verify mabbdad22 samples/operator-docs passes
reproduce mutastic run --only=mabbdad22 samples/operator-docs
or ignore mutastic ignore mabbdad22 --reason="<why>" samples/operator-docs

The test that catches it: one with a mix, some elements that match and some that don’t. That’s the only input where anyMatch and allMatch disagree.

@Test
void oneBlankAnswerAmongOthers() {
assertTrue(Forms.anyBlank(List.of("yes", " ")));
}

Edition notes: the Rust edition has this operator too, stream/swap-match, for an iterator’s any and all.

stream/remove-other-stage, in the extended set. peek is removed, and so is map on an IntStream, LongStream or DoubleStream.

The bug it stands for: a conversion that’s wrong, or forgotten, in a pipeline of numbers, and a peek that does real work someone relies on. It costs in totals, where a map adds the tax in one call and a test of an order with no tax due passes without it, and in a peek that records what went by.

/// The total, with each value written to `log` as it's added.
public static int logged(List<Integer> values, List<Integer> log) {
return values.stream().peek(log::add).mapToInt(Integer::intValue).sum();
}

The test checks the total, and never the log:

@Test
void totalsAddUp() {
assertEquals(3, Totals.logged(List.of(1, 2), new ArrayList<>()));
}
Terminal window
✗ MISSED no test checks what .peek(…) does in logged()
┌─ src/main/java/com/example/streams/Totals.java:8
│
7 │ public static int logged(List<Integer> values, List<Integer> log) {
8 │ return values.stream().peek(log::add).mapToInt(Integer::intValue).sum();
│ ^^^^ stage removed
9 │ }
│
= mutant - return values.stream().peek(log::add).mapToInt(Integer::intValue).sum();
+ return values.stream().mapToInt(Integer::intValue).sum();
= note: covered by TotalsTest#totalsAddUp (its 1 covering test still passes)
= help: add a test that asserts what .peek(…) does to each element
done when mutastic verify m2d698876 samples/operator-docs passes
reproduce mutastic run --only=m2d698876 samples/operator-docs
or ignore mutastic ignore m2d698876 --reason="<why>" samples/operator-docs

The test that catches it: one that checks what the stage does, asserting the result.

@Test
void eachValueIsLogged() {
var log = new ArrayList<Integer>();
Totals.logged(List.of(1, 2), log);
assertEquals(List.of(1, 2), log);
}

Edition notes: run it with --operators=defaults,extended. map on a Stream is left alone: it can change what the stream holds, and what comes next would throw on the wrong type, which would look like a catch. The Rust edition has this operator too, stream/remove-other-stage, for an iterator’s inspect and map.