Java edition
Arguments
Operators that change what passes between calls: a call left out of the way, arguments swapped, a parameter or an argument gone. Together they ask whether your tests’ values are ones each input makes a difference to.
| Operator | Changes | Set |
|---|---|---|
mutastic/omit-call | a call that returns what it’s given, left out: s.trim() to s | stronger |
mutastic/swap-arguments | a call’s last two arguments of the same type, swapped | stronger |
mutastic/null-parameter | a parameter holding an object, null from the start of the method | stronger |
mutastic/drop-vararg | the last value in an array built for a call, left out | stronger |
library/omit-argument | the JDK’s overloads, without their last argument: indexOf(x, from) to indexOf(x) | 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.
Omit call
Section titled “Omit call”mutastic/omit-call, in the stronger set. A call that returns the same type as what it’s given is
left out, and what it was given goes on in its place.
The bug it stands for: the step that was never written: the escape that isn’t around user
input, the strip that isn’t on a pasted value, the builder step left out. Real faults have this
shape, and the code reads as finished without it. It costs in anything that cleans or converts a
value before it’s used: on clean input the call makes no difference, and clean input is what tests
tend to have.
public static String username(String input) { return input.strip().toLowerCase(); } @Test void usernamesAreLowerCase() { assertEquals("ann", Users.username("Ann")); } ✗ MISSED no test needs strip() in username() ┌─ src/main/java/com/example/arguments/Users.java:5 │ 4 │ public static String username(String input) { 5 │ return input.strip().toLowerCase(); │ ^^^^^ call left out 6 │ } │ = mutant - return input.strip().toLowerCase(); + return input.toLowerCase(); = note: covered by UsersTest#usernamesAreLowerCase (its 1 covering test still passes) = help: add a test with a value strip() changes, and assert the result
done when mutastic verify maa5d9e7e samples/operator-docs passes reproduce mutastic run --only=maa5d9e7e samples/operator-docs or ignore mutastic ignore maa5d9e7e --reason="<why>" samples/operator-docsThe test that catches it: one with a value the call changes: text with spaces to strip,
something to escape, a negative number for Math.abs.
@Test void spacesArentPartOfAUsername() { assertEquals("ann", Users.username(" Ann ")); }Edition notes: it only leaves out calls it can be sure of: String’s and the string builders’
own, Math.abs, and your code’s own methods that return the type they’re called on or their one
argument, and have no type parameters. A library’s calls are left alone, as Stream.map returns a
Stream of something else. So is a call whose result is thrown away, which
remove discarded call removes. The Rust edition
has this operator too, mutastic/omit-call, for trim, abs, to_lowercase and the like, a
builder’s (self) -> Self step, and a crate function returning its one argument’s type.
Swap arguments
Section titled “Swap arguments”mutastic/swap-arguments, in the stronger set. A call’s last two arguments swap places, where they
have the same type.
The bug it stands for: two arguments the wrong way round. When they have the same type the compiler has nothing to say, and a test whose values are equal, or whose result doesn’t depend on the order, has nothing to say either. It costs in anything with a direction: a transfer’s source and destination, a range’s start and end, a width and a height, a replacement’s old and new text.
/// `level`, kept between the quietest and the loudest. public int set(int level) { return clamp(level, min, max); }
static int clamp(int value, int low, int high) { return Math.max(low, Math.min(value, high)); }Too loud is turned down to the loudest either way round:
@Test void tooLoudIsTurnedDown() { assertEquals(10, new Volume(0, 10).set(15)); } ✗ MISSED no test notices the last two arguments of clamp() swapped in set() ┌─ src/main/java/com/example/arguments/Volume.java:14 │ 13 │ public int set(int level) { 14 │ return clamp(level, min, max); │ ^^^^^ arguments swapped 15 │ } │ = mutant - return clamp(level, min, max); + return clamp(level, max, min); = note: inputs seen: level = 15 covered by VolumeTest#tooLoudIsTurnedDown (its 1 covering test still passes) = help: add a test that passes clamp() different values for them, and assert the result
// in VolumeTest @Test void setWithLevel0ForMin0AndMax10() { var volume = new Volume(0, 10); // as VolumeTest#tooLoudIsTurnedDown builds it assertEquals(0, volume.set(0)); // today 0 · with this mutant 10 } check today's value is the one you intend: the test pins what the code does now
done when mutastic verify m973063e5 samples/operator-docs passes reproduce mutastic run --only=m973063e5 samples/operator-docs or ignore mutastic ignore m973063e5 --reason="<why>" samples/operator-docsThe test that catches it: one that passes different values and checks which went where: here, a level in between, which should stay as it is.
@Test void aLevelInRangeIsKept() { assertEquals(4, new Volume(0, 10).set(4)); }Edition notes: only the last two: an earlier pair has other arguments on top of it when the call
is made. Arguments typed Object are left alone, as that’s what a type variable compiles to, and
map.put(key, value) swapped wouldn’t compile. A call that gives the same result either way round,
Math.max(a, b), or that passes one value twice, f(x, x), is skipped as equivalent. The Rust
edition has this operator too, mutastic/swap-arguments, for a crate function or method whose last
two parameters have one type.
Null parameter
Section titled “Null parameter”mutastic/null-parameter, in the stronger set. A parameter that holds an object is null from the
start of the method.
The bug it stands for: a value that’s handed in and doesn’t matter as it should: a parameter
ignored on one path, or a caller’s null going further than it should before it’s noticed. It costs
in code with many inputs where tests pin only some.
public record Booking(String guest, String room) {}
public static Booking book(String guest, String room) { return new Booking(guest, room); }The test checks whose booking it is, and never which room:
@Test void aBookingIsInTheGuestsName() { assertEquals("Ann", Bookings.book("Ann", "12").guest()); } ✗ MISSED no test depends on the value of room in book() ┌─ src/main/java/com/example/arguments/Bookings.java:7 │ 6 │ public static Booking book(String guest, String room) { 7 │ return new Booking(guest, room); │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ parameter is null 8 │ } │ = change room is null from the start of the method (not shown in the source) = note: covered by BookingsTest#aBookingIsInTheGuestsName (its 1 covering test still passes) = help: add a test whose result depends on room, and assert it
done when mutastic verify me9704166 samples/operator-docs passes reproduce mutastic run --only=me9704166 samples/operator-docs or ignore mutastic ignore me9704166 --reason="<why>" samples/operator-docsThe test that catches it: one whose result depends on the parameter, on the path where it’s used.
@Test void aBookingIsForTheRoomAsked() { assertEquals("12", Bookings.book("Ann", "12").room()); }Edition notes: a parameter whose value nothing reads, as it’s unused or assigned first, is
skipped as equivalent. Code compiled without its table of local variables has none, as nothing says
what they’re called. A method that rejects null straight away catches every one of these, which
says little about its tests: this operator costs a run, and finds most where a parameter is used
only on some paths. The Rust edition has this operator too, mutastic/null-parameter, for an
Option parameter, which becomes None, and adds mutastic/default-parameter, which gives a
parameter its type’s default.
Drop vararg
Section titled “Drop vararg”mutastic/drop-vararg, in the stronger set. The last value in an array built for a call is left
out. That’s usually a varargs call’s.
The bug it stands for: a value left out of a list: the role not in the set, the last column not in the query, the argument not in the message. It costs in lists built in one expression, where one element’s absence shows only for inputs that need it.
public static int bestOfThree(int first, int second, int third) { return Collections.max(Arrays.asList(first, second, third)); }The test’s best score comes first, so the last never matters:
@Test void theBestComesFirst() { assertEquals(9, Scores.bestOfThree(9, 2, 1)); } ✗ MISSED no test needs the last value given to asList() in bestOfThree() ┌─ src/main/java/com/example/arguments/Scores.java:8 │ 7 │ public static int bestOfThree(int first, int second, int third) { 8 │ return Collections.max(Arrays.asList(first, second, third)); │ ^^^^^^ last value left out 9 │ } │ = mutant - return Collections.max(Arrays.asList(first, second, third)); + return Collections.max(Arrays.asList(first, second)); = note: inputs seen: first = 9; second = 2; third = 1 covered by ScoresTest#theBestComesFirst (its 1 covering test still passes) = help: add a test whose result depends on the last value given to asList(), and assert it
// in ScoresTest @Test void bestOfThreeWithFirst9AndSecond9AndThird10() { assertEquals(10, Scores.bestOfThree(9, 9, 10)); // today 10 · with this mutant 9 } check today's value is the one you intend: the test pins what the code does now
done when mutastic verify mdc01ea8c samples/operator-docs passes reproduce mutastic run --only=mdc01ea8c samples/operator-docs or ignore mutastic ignore mdc01ea8c --reason="<why>" samples/operator-docsThe test that catches it: one that depends on the last value, not only on the others.
@Test void theBestComesLast() { assertEquals(9, Scores.bestOfThree(1, 2, 9)); }Edition notes: the value is still worked out, so a call in it still runs. An array that wasn’t
built on the spot, or was built with nothing in it, is left alone. List.of(a, b) has nothing to
drop: it has its own overload for a few values, with no array. The Rust edition has this operator
too, mutastic/drop-vararg, in extended, for &[a, b, c] and vec![a, b, c], as an array’s
length is part of its type there and more of its mutants don’t compile.
Omit argument
Section titled “Omit argument”library/omit-argument, in the stronger set. A call to one of the JDK’s overloads leaves out its
last argument and calls the shorter one.
The bug it stands for: the shorter overload where the longer was meant: a search from the start instead of from a position, hex read as decimal, a pattern losing its flags. It costs in parsing, where these reads run, and often return something plausible, on inputs the argument makes no difference to.
/// Where the next field ends: the first comma at or after `from`. public static int nextComma(String line, int from) { return line.indexOf(',', from); }The test only searches from the start, where the first comma is the first comma either way:
@Test void theFirstFieldEndsAtItsComma() { assertEquals(1, Csv.nextComma("a,b", 0)); } ✗ MISSED no test needs the last argument of indexOf in nextComma() ┌─ src/main/java/com/example/arguments/Csv.java:6 │ 5 │ public static int nextComma(String line, int from) { 6 │ return line.indexOf(',', from); │ ^^^^^^^ called without its last argument 7 │ } │ = mutant - return line.indexOf(',', from); + return line.indexOf(','); = note: inputs seen: from = 0 covered by CsvTest#theFirstFieldEndsAtItsComma (its 1 covering test still passes) = help: use a value that argument makes a difference to, and assert the result
done when mutastic verify macc6b02b samples/operator-docs passes reproduce mutastic run --only=macc6b02b samples/operator-docs or ignore mutastic ignore macc6b02b --reason="<why>" samples/operator-docsThe test that catches it: one with a value the argument changes: a search that starts past a match.
@Test void theSecondFieldEndsAtTheNextComma() { assertEquals(3, Csv.nextComma("a,b,c", 2)); }Edition notes: Java only: Rust has no overloading. It covers indexOf, lastIndexOf and
substring on a String or StringBuilder, parseInt, parseLong and toString with a radix,
Pattern.compile with flags, and Objects.toString with a default. It leaves out a charset, a
locale and a split limit, whose absence rarely shows.