Skip to content
mutastic

Java edition

Library methods

Operators that swap a library method for the one beside it, and change the text in your code: a string, or a regular expression. Together they ask whether your tests’ text is text where the difference would show.

OperatorChangesSet
library/swap-stringa String method and the similar one: startsWith and endsWith, toUpperCase and toLowerCasedefaults
library/swap-index-ofindexOf and lastIndexOfstronger
library/swap-mathMath.min and Math.max, Math.floor and Math.ceildefaults
mutastic/string-constanta string in the code, to ""stronger
mutastic/regex-constanta regular expression, to one that differs and still compilesextended

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.

library/swap-string, in the defaults. A String method becomes the similar one: startsWith and endsWith, toUpperCase and toLowerCase, stripLeading and stripTrailing.

The bug it stands for: the method next to the one you meant. They have the same arguments, the compiler can’t tell them apart, and on the text tests usually pick they give the same answer. It costs in parsing and normalising: a prefix check that’s really a suffix check, a key that’s upper case where the lookup expects lower, a value with its trailing space left on.

public static boolean isComment(String line) {
return line.startsWith("#");
}

The test’s comment is a # alone, which starts and ends the line:

@Test
void aCommentAndAPlainLine() {
assertTrue(Comments.isComment("#"));
assertFalse(Comments.isComment("hello"));
}
Terminal window
✗ MISSED no test tells startsWith from endsWith in isComment()
┌─ src/main/java/com/example/library/Comments.java:5
│
4 │ public static boolean isComment(String line) {
5 │ return line.startsWith("#");
│ ^^^^^^^^^^ startsWith changed to endsWith
6 │ }
│
= mutant - return line.startsWith("#");
+ return line.endsWith("#");
= note: covered by CommentsTest#aCommentAndAPlainLine (its 1 covering test still passes)
= help: use text with it at one end and not the other, and assert the result
done when mutastic verify m7d64aafa samples/operator-docs passes
reproduce mutastic run --only=m7d64aafa samples/operator-docs
or ignore mutastic ignore m7d64aafa --reason="<why>" samples/operator-docs

The test that catches it: text with what’s looked for at one end only, letters whose case shows, a space on one side only.

@Test
void aCommentStartsWithTheHash() {
assertTrue(Comments.isComment("# a note"));
}

Edition notes: the Rust edition has this operator too, library/swap-string, for starts_with and ends_with, to_uppercase and to_lowercase, trim_start and trim_end, and strip_prefix and strip_suffix.

library/swap-index-of, in the stronger set. String.indexOf and lastIndexOf swap places.

The bug it stands for: the first match where the last was meant. On text with one match they agree, and that’s the text tests usually have. It costs in taking text apart: a file’s extension taken from its first dot, a package name cut at the wrong separator. It’s in stronger because much of what it finds can’t happen: text that has the character once by its format.

public static String extension(String file) {
return file.substring(file.indexOf('.') + 1);
}
@Test
void aTextFile() {
assertEquals("txt", Files.extension("notes.txt"));
}
Terminal window
✗ MISSED no test tells indexOf from lastIndexOf in extension()
┌─ src/main/java/com/example/library/Files.java:5
│
4 │ public static String extension(String file) {
5 │ return file.substring(file.indexOf('.') + 1);
│ ^^^^^^^ indexOf changed to lastIndexOf
6 │ }
│
= mutant - return file.substring(file.indexOf('.') + 1);
+ return file.substring(file.lastIndexOf('.') + 1);
= note: covered by FilesTest#aTextFile (its 1 covering test still passes)
= help: use text with what's searched for in it twice, and assert which one is found
done when mutastic verify m486323be samples/operator-docs passes
reproduce mutastic run --only=m486323be samples/operator-docs
or ignore mutastic ignore m486323be --reason="<why>" samples/operator-docs

The test that catches it: one where what’s searched for appears twice. Here it says which dot the extension starts at.

@Test
void anArchivesExtensionIsEverythingAfterTheName() {
assertEquals("tar.gz", Files.extension("backup.tar.gz"));
}

Edition notes: a search whose result is only tested for being there, name.indexOf('.') >= 0, is skipped as equivalent, as both find it or neither does. A search with a starting point, indexOf(x, from), is left alone. The Rust edition has this operator too, library/swap-index-of, for find and rfind, position and rposition, split_once and rsplit_once, and match_indices and rmatch_indices.

library/swap-math, in the defaults. Math.min and Math.max swap places, and so do Math.floor and Math.ceil.

The bug it stands for: min where max was meant, a limit that’s a floor instead of a ceiling, or rounding the wrong way. Each reads right at a glance, and tests whose values are all on one side of the limit, or whole numbers, can’t tell. It costs in limits, clamps, page counts and money: a batch larger than its limit, a page count one short, an amount rounded in the customer’s favour.

/// Never more off than the cap.
public int capped(int percent) {
return Math.min(percent, cap);
}

The test asks for exactly the cap, where min and max agree:

@Test
void halfOffAtMost() {
assertEquals(50, new Discounts(50).capped(50));
}
Terminal window
✗ MISSED no test tells min from max in capped()
┌─ src/main/java/com/example/library/Discounts.java:12
│
11 │ public int capped(int percent) {
12 │ return Math.min(percent, cap);
│ ^^^ min changed to max
13 │ }
│
= mutant - return Math.min(percent, cap);
+ return Math.max(percent, cap);
= note: inputs seen: percent = 50
covered by DiscountsTest#halfOffAtMost (its 1 covering test still passes)
= help: use two different values, and assert which comes back
// in DiscountsTest
@Test
void cappedWithPercent0ForCap50() {
var discounts = new Discounts(50); // as DiscountsTest#halfOffAtMost builds it
assertEquals(0, discounts.capped(0)); // today 0 · with this mutant 50
}
check today's value is the one you intend: the test pins what the code does now
done when mutastic verify m70b89aa9 samples/operator-docs passes
reproduce mutastic run --only=m70b89aa9 samples/operator-docs
or ignore mutastic ignore m70b89aa9 --reason="<why>" samples/operator-docs

The test that catches it: one on the side the code guards against: two different values for min and max, and a value with a fraction for floor and ceil.

@Test
void moreThanTheCapIsCapped() {
assertEquals(50, new Discounts(50).capped(80));
}

Edition notes: rounding a whole number, Math.floor((double) count), is skipped as equivalent, as it rounds to itself either way. The Rust edition has this operator too, library/swap-math, for min and max, cmp::min and cmp::max, min_by and max_by, and floor and ceil, and adds library/reverse-ordering, which turns a.cmp(&b) into b.cmp(&a).

mutastic/string-constant, in the stronger set. A string in the code becomes "", and an empty one a word.

The bug it stands for: the wrong key, name or format: a typo in a "content-type", or a value someone changed in one place and not the other. It costs in text the code acts on: map keys, file names, protocol words, separators, what a value is compared with. The compiler can’t check any of it.

public static String header(List<String> columns) {
return String.join(", ", columns);
}

The test’s header has one column, so the separator never shows:

@Test
void oneColumn() {
assertEquals("name", Headers.header(List.of("name")));
}
Terminal window
✗ MISSED no test tells ", " from "" in header()
┌─ src/main/java/com/example/library/Headers.java:7
│
6 │ public static String header(List<String> columns) {
7 │ return String.join(", ", columns);
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ", " changed to ""
8 │ }
│
= mutant - return String.join(", ", columns);
+ return String.join("", columns);
= note: covered by HeadersTest#oneColumn (its 1 covering test still passes)
= help: add a test whose result depends on ", ", and assert it
done when mutastic verify m0d61f3d0 samples/operator-docs passes
reproduce mutastic run --only=m0d61f3d0 samples/operator-docs
or ignore mutastic ignore m0d61f3d0 --reason="<why>" samples/operator-docs

The test that catches it: one whose result depends on the text.

@Test
void columnsAreSeparatedByCommas() {
assertEquals("name, email", Headers.header(List.of("name", "email")));
}

Edition notes: run it with --operators=stronger. Text that only tells a person something is skipped as arid: what goes into a log call or System.out, an exception’s message, including through String.format or formatted, or a failed check’s message. An exception is known by being thrown straight away, or by its supertypes reaching Throwable: a class named ConfigError that isn’t one keeps its mutant, and so does one whose supertypes can’t be found. A failed check is Objects.requireNonNull or Guava’s Preconditions. A string in an annotation isn’t in the code, and the text in "total: " + x is in javac’s recipe for the call, so neither has a mutant. Text a person reads in other ways, such as a command’s output, is changed like any other, so expect misses there: ranking puts those after the rest, and most aren’t worth a test. The Rust edition has this operator too, mutastic/string-constant, and leaves alone an error’s or expect’s message.

mutastic/regex-constant, in the extended set. A regular expression changes, one mutant for each place: an anchor (^ or $) goes, a character class or \d, \w or \s turns the other way round, or a quantifier goes.

The bug it stands for: an expression that accepts more than it should, or less: a missing anchor, a + where one was enough, a class the wrong way round. It costs in validation and parsing: tests of a regular expression tend to try text that should match, and an unanchored \d+ finds abc123 too.

/// An order number is digits, and nothing else.
public final class OrderNumbers {
private static final Pattern DIGITS = Pattern.compile("^\\d+$");
public static boolean isValid(String text) {
return DIGITS.matcher(text).find();
}
}

The test tries digits, and letters alone, but never the two together, so neither anchor is needed:

@Test
void digitsAreAnOrderNumber() {
assertTrue(OrderNumbers.isValid("123"));
assertFalse(OrderNumbers.isValid("abc"));
}
Terminal window
✗ MISSED no test tells ^\d+$ from \d+$ in a static field or initialiser · m8d47a622
┌─ src/main/java/com/example/library/OrderNumbers.java:7
│
6 │ public final class OrderNumbers {
7 │ private static final Pattern DIGITS = Pattern.compile("^\\d+$");
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^\d+$ changed to \d+$
8 │
│
= mutant - private static final Pattern DIGITS = Pattern.compile("^\\d+$");
+ private static final Pattern DIGITS = Pattern.compile("\\d+$");
= also ✗ no test tells ^\d+$ from ^\d+ in a static field or initialiser · mda52fc08
+ private static final Pattern DIGITS = Pattern.compile("^\\d+");
= note: covered by OrderNumbersTest#digitsAreAnOrderNumber (its 1 covering test still passes)
= help: add a test with text only one of ^\d+$ and \d+$ matches, and assert it
done when mutastic verify m8d47a622,mda52fc08 samples/operator-docs passes
reproduce mutastic run --only=m8d47a622,mda52fc08 samples/operator-docs
or ignore mutastic ignore <id> --reason="<why>" samples/operator-docs for each of m8d47a622, mda52fc08

The test that catches it: text that only one of the two matches, at each end.

@Test
void lettersAroundTheDigitsAreNotAnOrderNumber() {
assertFalse(OrderNumbers.isValid("abc123"));
assertFalse(OrderNumbers.isValid("123abc"));
}

Edition notes: run it with --operators=defaults,extended. It only changes text that goes straight into Pattern.compile, Pattern.matches, matches, split, replaceAll or replaceFirst, and only to an expression that compiles, so what doesn’t look like a regular expression has no mutants. It’s the one operator that also runs in constructors and static initialisers, as a Pattern is usually a field. An anchor is skipped as equivalent on matches, which has to match all of the text anyway. The Rust edition has this operator too, mutastic/regex-constant, for Regex::new.