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.
| Operator | Changes | Set |
|---|---|---|
mutastic/remove-void-call | a call that returns nothing, removed | defaults |
mutastic/remove-discarded-call | a call whose result is thrown away, removed | defaults |
mutastic/remove-assignment | an assignment to a variable that already has a value, removed | defaults |
mutastic/remove-field-write | a write to a field, removed | stronger |
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.
Remove void call
Section titled “Remove void call”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"))); } ✗ 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-docsThe 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).
Remove discarded call
Section titled “Remove discarded call”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()); } ✗ 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-docsThe 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();.
Remove assignment
Section titled “Remove assignment”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)); } ✗ 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, m3befb2c0The 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 _ = …;.
Remove field write
Section titled “Remove field write”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()); } ✗ 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-docsThe 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 }).