Skip to content
mutastic

Java edition

Calls and writes

Operators that leave a step out: a call made for what it does, an assignment, a write to a field. Together they ask whether your tests check what the code does, not only what it returns.

OperatorChangesSet
mutastic/remove-void-calla call that returns nothing, removeddefaults
mutastic/remove-discarded-calla call whose result is thrown away, removeddefaults
mutastic/remove-assignmentan assignment to a variable that already has a value, removeddefaults
mutastic/remove-field-writea write to a field, removedstronger

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.

mutastic/remove-void-call, in the defaults. A call that returns nothing is removed.

The bug it stands for: a step left out: a call removed in a refactor, never added, or skipped on one path. It costs in side effects, which are often the point of the code: saving a record, reserving stock, sending an email, writing an audit entry, releasing a lock, sorting before a search. If removing the call passes every test, nothing checks the step happens.

public static List<String> sorted(List<String> names) {
var copy = new ArrayList<>(names);
Collections.sort(copy);
return copy;
}

The test’s names are in order already:

@Test
void namesInOrder() {
assertEquals(List.of("al", "bo"), Names.sorted(List.of("al", "bo")));
}
Terminal window
✗ MISSED no test checks the effect of calling sort() in sorted()
┌─ src/main/java/com/example/calls/Names.java:10
│
9 │ var copy = new ArrayList<>(names);
10 │ Collections.sort(copy);
│ ^^^^ call removed
11 │ return copy;
│
= mutant - Collections.sort(copy);
= note: covered by NamesTest#namesInOrder (its 1 covering test still passes)
= help: assert what sort() does: the state it changes or the call it makes
done when mutastic verify m6e073afd samples/operator-docs passes
reproduce mutastic run --only=m6e073afd samples/operator-docs
or ignore mutastic ignore m6e073afd --reason="<why>" samples/operator-docs

The test that catches it: one that checks the effect, here an order that has to change, rather than only that the method returned.

@Test
void namesAreSorted() {
assertEquals(List.of("al", "bo", "cy"), Names.sorted(List.of("cy", "al", "bo")));
}

Edition notes: the Rust edition has this operator too, mutastic/remove-void-call, for a call the crate declares with no return, or one of std’s (push, sort, clear, extend, drop and the like).

mutastic/remove-discarded-call, in the defaults. A call whose result is thrown away is removed: items.add(item);, seen.put(key, value);, out.append(text);. Its result isn’t used, so only what it does matters.

The bug it stands for: the same as a removed void call: a step left out. Many calls that do something also return something nobody needs: add and remove on a collection say whether it changed, put returns what was there, a builder’s append returns the builder. These hide best, since a call written for its effect reads like a query.

/// A receipt: each item on a line of its own.
public static String text(List<String> items) {
var out = new StringBuilder();
for (String item : items) {
out.append(item);
out.append(System.lineSeparator());
}
return out.toString();
}

The test has one item, and strips the receipt before it looks:

@Test
void oneItem() {
assertEquals("tea", Receipts.text(List.of("tea")).strip());
}
Terminal window
✗ MISSED no test checks the effect of calling append() in text()
┌─ src/main/java/com/example/calls/Receipts.java:11
│
10 │ out.append(item);
11 │ out.append(System.lineSeparator());
│ ^^^^^^ call removed
12 │ }
│
= mutant - out.append(System.lineSeparator());
+ System.lineSeparator();
= note: covered by ReceiptsTest#oneItem (its 1 covering test still passes)
= help: assert what append() does: the state it changes or the call it makes
done when mutastic verify md837696f samples/operator-docs passes
reproduce mutastic run --only=md837696f samples/operator-docs
or ignore mutastic ignore md837696f --reason="<why>" samples/operator-docs

The test that catches it: one that checks the effect: here, that two items come out on two lines.

@Test
void eachItemOnItsOwnLine() {
assertEquals(List.of("tea", "milk"), Receipts.text(List.of("tea", "milk")).lines().toList());
}

Edition notes: Objects.requireNonNull(x);, which only checks a value isn’t null, is skipped as arid. Constructors are left alone: removing one would leave an object half made, not a gap in the tests. The Rust edition has this operator too, mutastic/remove-discarded-call, for map.insert(k, v);, w.flush()?; and let _ = f();.

mutastic/remove-assignment, in the defaults. An assignment to a variable that already has a value is removed, so the variable keeps that value. What it would have been given is still worked out, so a call on the right-hand side still runs.

The bug it stands for: a step left out of a calculation: a running total that isn’t updated on one path, a value worked out and then not kept, a variable reset in the wrong place. It costs in totals, accumulators and anything built up over several statements: the code still runs and still returns something, only from a value that’s out of date.

public static int withTip(int bill, int tip) {
int total = bill;
total = total + tip;
return total;
}

The test leaves no tip, so the total is right whether it’s updated or not:

@Test
void noTip() {
assertEquals(10, Tips.withTip(10, 0));
}
Terminal window
✗ MISSED tests pass even when + becomes - in withTip() · md535f9c9
┌─ src/main/java/com/example/calls/Tips.java:6
│
5 │ int total = bill;
6 │ total = total + tip;
│ ^ + changed to -
7 │ return total;
│
= mutant - total = total + tip;
+ total = total - tip;
= also ✗ no test needs tip in total + tip in withTip() · m5a450f64
+ total = total;
= also ✗ no test checks the value total is given in withTip() · m3befb2c0
= change the assignment to total is removed, so it keeps its earlier value
= note: inputs seen: bill = 10; tip = 0
covered by TipsTest#noTip (its 1 covering test still passes)
= help: use inputs where total + tip and total - tip differ, and assert the result
// in TipsTest
@Test
void withTipWithBill10AndTip10() {
assertEquals(20, Tips.withTip(10, 10)); // today 20 · with md535f9c9 0
}
check today's value is the one you intend: the test pins what the code does now
done when mutastic verify md535f9c9,m5a450f64,m3befb2c0 samples/operator-docs passes
reproduce mutastic run --only=md535f9c9,m5a450f64,m3befb2c0 samples/operator-docs
or ignore mutastic ignore <id> --reason="<why>" samples/operator-docs for each of md535f9c9, m5a450f64, m3befb2c0

The test that catches it: one whose result depends on the new value, not one where the earlier value happens to be right.

@Test
void aTipIsAdded() {
assertEquals(12, Tips.withTip(10, 2));
}

Edition notes: a variable’s first value is left alone, as the code wouldn’t run without it. So is code compiled without its table of local variables (-g:none, or no vars in -g), which doesn’t say what’s a variable. An assignment whose value nothing reads, because the variable is assigned again first or never used, is skipped as equivalent. i++ isn’t an assignment here: it’s flip increment’s. The Rust edition has this operator too, mutastic/remove-assignment, and keeps the right-hand side as let _ = …;.

mutastic/remove-field-write, in the stronger set. A write to a field is removed, so the field keeps the value it had.

The bug it stands for: state that never changes: an update left out, or a write lost in a refactor, so the object goes on saying what it said before. It costs in anything an object has to remember between calls: a status moved on, a balance, a cache kept, a flag that stops something happening twice. Each call looks fine alone; what’s wrong only shows in the next one.

/// Marks the order paid, and gives what was due.
public int pay() {
this.paid = true;
return total;
}

The test checks what paying gives back, and never asks whether the order is paid:

@Test
void payingGivesTheTotal() {
assertEquals(30, new Order(30).pay());
}
Terminal window
✗ MISSED no test checks the value paid is given in pay()
┌─ src/main/java/com/example/calls/Order.java:13
│
12 │ public int pay() {
13 │ this.paid = true;
│ ^^^^^^^^^^^^^^^^^ write removed
14 │ return total;
│
= mutant - this.paid = true;
= note: covered by OrderTest#payingGivesTheTotal (its 1 covering test still passes)
= help: add a test where this gives paid a different value, and assert what depends on it
done when mutastic verify md94e0e3d samples/operator-docs passes
reproduce mutastic run --only=md94e0e3d samples/operator-docs
or ignore mutastic ignore md94e0e3d --reason="<why>" samples/operator-docs

The test that catches it: one that makes the change and then looks: at the field, or at what the object does differently now.

@Test
void payingMarksTheOrderPaid() {
var order = new Order(30);
order.pay();
assertTrue(order.isPaid());
}

Edition notes: run it with --operators=stronger. Constructors and static initialisers are left alone, as they are for every operator, and a setter with nothing else in it is skipped as arid. A write of a default value, count = 0; in a reset(), is kept: that’s what reset() is for. The Rust edition has this operator too, mutastic/remove-field-write, for self.status = s;, *p = v; and v[i] = x;, and adds mutastic/remove-struct-field, which leaves a field out of a struct update (S { a, ..base }).