Java edition
Conditions
Operators that change a comparison, a condition, or how two conditions combine. Together they ask whether your tests look at both sides of every decision, and at its edge.
| Operator | Changes | Set |
|---|---|---|
mutastic/boundary | < and <=, > and >= | defaults |
mutastic/comparison | a comparison to the one that differs on one side: < to != | stronger |
mutastic/other-comparison | a comparison to the others, and a condition to its opposite | stronger |
mutastic/remove-conditional | a condition to always true, or always false | defaults |
mutastic/logical-connector | && and || | defaults |
predicate/swap-connector | Predicate.and and or | extended |
predicate/remove-negate | a predicate’s negate(), removed | extended |
predicate/remove-not | Predicate.not(p) to p | extended |
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.
Boundary
Section titled “Boundary”mutastic/boundary, in the defaults. < becomes <=, > becomes >=, and the other way round.
The bug it stands for: the off-by-one. >= and > read almost the same, and reviews rarely
stop on them, so the wrong one gets written and gets through. It costs wherever a rule has a limit:
free delivery from 50, a discount from 10 items, a login locked after 3 failures, the last day a
refund is allowed. Customers are at the edge all the time: someone who spends exactly 50 because
the site said “free delivery from 50” is the case the rule exists for.
public boolean isFree(int total) { return total >= freeFrom; }The test tries an order on each side, and neither at the edge:
@Test void smallOrdersPayForDelivery() { var delivery = new Delivery(50); assertFalse(delivery.isFree(20)); assertTrue(delivery.isFree(80)); } ✗ MISSED no test checks isFree() at total == freeFrom ┌─ src/main/java/com/example/conditions/Delivery.java:12 │ 11 │ public boolean isFree(int total) { 12 │ return total >= freeFrom; │ ^^ >= changed to > 13 │ } │ = mutant - return total >= freeFrom; + return total > freeFrom; = note: inputs seen: total = 20, 80, with freeFrom = 50 (Delivery's field, from new Delivery(50)) none has total == freeFrom covered by DeliveryTest#smallOrdersPayForDelivery (its 1 covering test still passes) = help: test isFree() with total = freeFrom and assert the result
// in DeliveryTest @Test void isFreeAtFreeFrom() { var delivery = new Delivery(50); // as DeliveryTest#smallOrdersPayForDelivery builds it assertTrue(delivery.isFree(50)); // total == freeFrom · today true · with this mutant false } check today's value is the one you intend: the test pins what the code does now
done when mutastic verify m6c808715 samples/operator-docs passes reproduce mutastic run --only=m6c808715 samples/operator-docs or ignore mutastic ignore m6c808715 --reason="<why>" samples/operator-docsThe test that catches it: one at the edge. Totals of 20 and 80 pass either way; only 50 tells
>= from >. The card’s inputs seen show which values the tests tried, so the missing edge is easy
to spot. A boundary miss is also a question for whoever owns the rule: should 50 get free delivery?
The test records the answer.
@Test void deliveryIsFreeFromExactly50() { assertTrue(new Delivery(50).isFree(50)); }Edition notes: the Rust edition has this operator too, mutastic/boundary, and also moves the
end of a range, lo..hi and lo..=hi, in a for, a slice or a contains.
Comparison
Section titled “Comparison”mutastic/comparison, in the stronger set. A comparison becomes the one that gives the same
answer at its edge and on one side of it, and the other answer on the other side:
| Written | Becomes | Differs where |
|---|---|---|
a < b | a != b | a > b |
a <= b | a == b | a < b |
a > b | a != b | a < b |
a >= b | a == b | a > b |
a == b | a <= b, and a >= b | a < b, and a > b |
a != b | a < b, and a > b | a > b, and a < b |
The bug it stands for: a comparison that’s right at the edge and wrong past it: == where >=
was meant, so a rule holds for exactly 100 and stops at 101. It costs in the same rules as a
boundary, on the side tests skip: tests often try a value below a limit and one exactly at it, and
never one well past it.
public boolean needsRestock(int stock) { return stock < low; }The test tries stock below the mark and at it, so the boundary’s mutant is caught, but never above it:
@Test void restockWhenLow() { var stock = new Stock(5); assertTrue(stock.needsRestock(2)); assertFalse(stock.needsRestock(5)); } ✗ MISSED no test checks needsRestock() with stock > low ┌─ src/main/java/com/example/conditions/Stock.java:12 │ 11 │ public boolean needsRestock(int stock) { 12 │ return stock < low; │ ^ < changed to != 13 │ } │ = mutant - return stock < low; + return stock != low; = note: inputs seen: stock = 2, 5 covered by StockTest#restockWhenLow (its 1 covering test still passes) = help: add a test where stock > low, and assert what needsRestock() does then
// in StockTest @Test void needsRestockWithStock10ForLow5() { var stock = new Stock(5); // as StockTest#restockWhenLow builds it assertFalse(stock.needsRestock(10)); // today false · with this mutant true } check today's value is the one you intend: the test pins what the code does now
done when mutastic verify md89cd49a samples/operator-docs passes reproduce mutastic run --only=md89cd49a samples/operator-docs or ignore mutastic ignore md89cd49a --reason="<why>" samples/operator-docsThe test that catches it: one on the side where the two disagree, which the card names: here, stock above the low mark.
@Test void plentyInStockNeedsNoRestock() { assertFalse(new Stock(5).needsRestock(8)); }Edition notes: with boundary and remove conditional, these make the smallest set of changes
that’s enough for a comparison: tests that catch the edge moved, the side changed and the condition
always true or false catch every other way it could be written wrong. For a < b the three are
a <= b, a != b and false, each one operator’s, so no condition gets the same change twice. Nor
does a length the JDK keeps from going negative (which ones):
name.length() > 0 becoming name.length() >= 0 is the condition always true, so it’s left to
remove conditional, and of two changes that agree for every length only the first is made
(boundary’s, then comparison’s, then other comparison’s).
Only numbers have an order: == on two booleans or references, and == null, get no mutant here.
A comparison of floats or doubles gets only changes that treat NaN as Java does, so x < y
gets none (x != y would differ for NaN too). A change that differs only for a negative length,
name.length() == 0 to name.length() <= 0, is skipped as equivalent where the JDK’s own code
gives the length. It’s in stronger
because, on mutastic’s own code, most of what it missed was noise: a loop counter that counts up to
its end (i < n and i != n stop at the same place), and a size compared with a value it never
passes. The Rust edition makes the same changes, and == and != get them only beside a number, a
character, a length or a cast.
Other comparison
Section titled “Other comparison”mutastic/other-comparison, in the stronger set. A comparison becomes each of the others, and any
condition its opposite: < becomes >, >= or ==; == becomes !=, < or >; == null
becomes != null; if (order.isPaid()) becomes if (!order.isPaid()).
The bug it stands for: a condition the wrong way round: a missing !, == for !=, a null
check that guards the wrong case. It costs where code decides which way to go: whether an order is
paid before it ships, whether a user may see a page, whether to retry. Getting it backwards usually
means doing the wrong thing in exactly the case that matters: shipping unpaid orders, or refusing
paid ones. Security checks deserve this most: a permission check whose opposite no test notices is
one nobody has proved refuses anyone.
public static List<Order> toShip(List<Order> orders) { var ship = new ArrayList<Order>(); for (Order order : orders) { if (order.paid()) { ship.add(order); } } return ship; }The test gives one paid order and one unpaid, and counts what ships, but never looks at which:
@Test void oneOrderOfTwoShips() { var orders = List.of(new Order("a1", true), new Order("b2", false)); assertEquals(1, Warehouse.toShip(orders).size()); } ✗ MISSED toShip() behaves the same with order.paid() reversed ┌─ src/main/java/com/example/conditions/Warehouse.java:13 │ 12 │ for (Order order : orders) { 13 │ if (order.paid()) { │ ^^^^^^^^^^^^^^^^^^^ reversed 14 │ ship.add(order); │ = mutant - if (order.paid()) { + if (!order.paid()) { = note: covered by WarehouseTest#oneOrderOfTwoShips (its 1 covering test still passes) = help: assert the result on both sides of order.paid()
done when mutastic verify m5f1f4e60 samples/operator-docs passes reproduce mutastic run --only=m5f1f4e60 samples/operator-docs or ignore mutastic ignore m5f1f4e60 --reason="<why>" samples/operator-docsThe test that catches it: one that checks which way each side goes: here, that the order that ships is the paid one.
@Test void onlyThePaidOrderShips() { var paid = new Order("a1", true); assertEquals(List.of(paid), Warehouse.toShip(List.of(paid, new Order("b2", false)))); }Edition notes: a test that catches the mutants of boundary, comparison and remove conditional
catches these too, so in the defaults their catches would only add to the score. It replaced
mutastic/negate-conditional, whose change is the condition’s opposite. The Rust edition has this
operator too, mutastic/other-comparison, and turns matches!(..) into !matches!(..) too.
Remove conditional
Section titled “Remove conditional”mutastic/remove-conditional, in the defaults. A condition stops deciding: the code it guards always
runs, or never does. Each condition gets two mutants, one each way.
The bug it stands for: a check that doesn’t matter as much as its author thought, or that’s the wrong check entirely. It costs in guards: a permission check, a validation, an “only if it’s changed” before a write, a retry that should stop. If the code behaves the same with the guard always passing, no test has ever been refused by it; if it behaves the same with the guard never passing, no test has ever needed it to let something through.
public static String greeting(boolean member) { if (member) { return "welcome back"; } return "welcome"; }The test only greets a member, so the condition always true gets past it:
@Test void membersAreWelcomedBack() { assertEquals("welcome back", Greetings.greeting(true)); } ✗ MISSED no test needs member to be false in greeting() ┌─ src/main/java/com/example/conditions/Greetings.java:5 │ 4 │ public static String greeting(boolean member) { 5 │ if (member) { │ ^^^^^^^^^^^^^ now always true 6 │ return "welcome back"; │ = mutant - if (member) { + if (true) { = note: covered by GreetingsTest#membersAreWelcomedBack (its 1 covering test still passes) = help: add a test where member is false, and assert what greeting() does then
done when mutastic verify m2ebb995b samples/operator-docs passes reproduce mutastic run --only=m2ebb995b samples/operator-docs or ignore mutastic ignore m2ebb995b --reason="<why>" samples/operator-docsThe test that catches it: one for each answer the condition can give. The card names the one no test has tried: here, someone who isn’t a member.
@Test void othersAreWelcomed() { assertEquals("welcome", Greetings.greeting(false)); }Edition notes: other comparison asks the same question from another angle; remove conditional is
clearer about which answer no test has tried, and catches a condition checked twice where one check
never decides. The Rust edition has this operator too, mutastic/remove-conditional, and reaches
while, a match arm’s guard, and let Some(x) = e in an if let, while let or let-else.
Logical connector
Section titled “Logical connector”mutastic/logical-connector, in the defaults. In a condition of two tests, && becomes || and
|| becomes &&.
The bug it stands for: && where || was meant, or the other way round: a rule that needs both
conditions written as either, a discount for members or big orders written as and. It costs in
eligibility and permission rules, validation that should reject on any of several problems, and
retries that stop on either of two signals. Tests that set both conditions true, or both false, pass
either way.
public static boolean canShip(boolean paid, boolean inStock) { return paid && inStock; } @Test void shipsWhenPaidAndInStock() { assertTrue(Shipping.canShip(true, true)); assertFalse(Shipping.canShip(false, false)); }The two give different answers exactly when one side holds and the other doesn’t, which is also when each side always true would show, so the card holds all three:
✗ MISSED no test needs paid to be false in canShip() · m43949e81 ┌─ src/main/java/com/example/conditions/Shipping.java:5 │ 4 │ public static boolean canShip(boolean paid, boolean inStock) { 5 │ return paid && inStock; │ ^^^^^^^^^^^^^^^^^^^^^^^ now always true 6 │ } │ = mutant - return paid && inStock; + return true && inStock; = also ✗ no test checks canShip() with one of paid and inStock true and the other false · mf79dc8de + return paid || inStock; = also ✗ no test needs inStock to be false in canShip() · mf006c884 + return paid && true; = note: covered by ShippingTest#shipsWhenPaidAndInStock (its 1 covering test still passes) = help: add a test where paid is false, and assert what canShip() does then
done when mutastic verify m43949e81,mf79dc8de,mf006c884 samples/operator-docs passes reproduce mutastic run --only=m43949e81,mf79dc8de,mf006c884 samples/operator-docs or ignore mutastic ignore <id> --reason="<why>" samples/operator-docs for each of m43949e81, mf79dc8de, mf006c884The test that catches it: one where the two conditions disagree, each way round: paid but out of stock, and in stock but unpaid.
@Test void neitherAloneIsEnough() { assertFalse(Shipping.canShip(true, false)); assertFalse(Shipping.canShip(false, true)); }Edition notes: the bytecode has no && or ||, only two tests that jump, so mutastic
recognises the way javac lays out a condition of two tests (in an if, a loop, a return, through
a !) and turns the first test’s jump round. It leaves alone a condition of three or more tests, an
if nested in another on a line of its own (it compiles the same as &&), and a test that reads
what the one before it binds, value instanceof Integer number && number > 0, which || couldn’t
run. When the statement has more than one && or || the card can’t say which, and says so. The
Rust edition has this operator too, mutastic/logical-connector, and reads the source, so it has
none of these limits.
Predicate connector swap
Section titled “Predicate connector swap”predicate/swap-connector, in the extended set. and and or swap places, on Predicate,
BiPredicate and the primitive predicates.
The bug it stands for: “and” where “or” was meant, in a condition that’s built up rather than written out. It costs in search filters and access rules composed from smaller predicates: a filter that shows too much, or nothing at all.
public static Predicate<String> held(Predicate<String> shouted, Predicate<String> isLong) { return shouted.and(isLong); }The test has a message that’s both, and one that’s neither:
@Test void aLongShoutIsHeld() { Predicate<String> shouted = text -> text.equals(text.toUpperCase()); Predicate<String> isLong = text -> text.length() > 10; var held = Moderation.held(shouted, isLong); assertTrue(held.test("PLEASE READ THIS")); assertFalse(held.test("hi")); } ✗ MISSED no test tells and from or between two predicates in held() ┌─ src/main/java/com/example/conditions/Moderation.java:8 │ 7 │ public static Predicate<String> held(Predicate<String> shouted, Predicate<String> isLong) { 8 │ return shouted.and(isLong); │ ^^^ and changed to or 9 │ } │ = mutant - return shouted.and(isLong); + return shouted.or(isLong); = note: covered by ModerationTest#aLongShoutIsHeld (its 1 covering test still passes) = help: use a value one of the two predicates accepts and the other doesn't, and assert the result
done when mutastic verify mc194dcd9 samples/operator-docs passes reproduce mutastic run --only=mc194dcd9 samples/operator-docs or ignore mutastic ignore mc194dcd9 --reason="<why>" samples/operator-docsThe test that catches it: a value one of the two accepts and the other doesn’t: a short shout.
@Test void aShortShoutIsLetThrough() { Predicate<String> shouted = text -> text.equals(text.toUpperCase()); assertFalse(Moderation.held(shouted, text -> text.length() > 10).test("HI")); }Edition notes: Java only: Rust’s std has no Predicate combinators, and && inside a closure
is logical connector’s there. Run it with --operators=defaults,extended.
Predicate negation removal
Section titled “Predicate negation removal”predicate/remove-negate and predicate/remove-not, in the extended set. A predicate’s negate()
is removed, and Predicate.not(p) becomes p, so it accepts what it rejected.
The bug it stands for: a condition the wrong way round, written as a method rather than a !. It
costs in anything filtered by what it isn’t: the blank lines dropped, the banned users kept out.
Without the negation it keeps exactly what it should drop.
private static final Predicate<String> BLANK = String::isBlank;
/// How many lines have something on them. public static long written(List<String> lines) { return lines.stream().filter(BLANK.negate()).count(); }The test has one line of each, and counts what’s kept: one, whichever way round the filter is.
@Test void blankLinesDontCount() { assertEquals(1, Lines.written(List.of("total: 3", " "))); } ✗ MISSED no test checks which values the negated predicate accepts in written() ┌─ src/main/java/com/example/conditions/Lines.java:11 │ 10 │ public static long written(List<String> lines) { 11 │ return lines.stream().filter(BLANK.negate()).count(); │ ^^^^^^ no longer negated 12 │ } │ = mutant - return lines.stream().filter(BLANK.negate()).count(); + return lines.stream().filter(BLANK).count(); = note: covered by LinesTest#blankLinesDontCount (its 1 covering test still passes) = help: use a value the predicate accepts and one it rejects, and assert the result for each
done when mutastic verify mbcae9b5a samples/operator-docs passes reproduce mutastic run --only=mbcae9b5a samples/operator-docs or ignore mutastic ignore mbcae9b5a --reason="<why>" samples/operator-docsPredicate.not is the same bug, written the other way:
/// How many of a form's fields are filled in. public static long filled(List<String> values) { return values.stream().filter(Predicate.not(String::isEmpty)).count(); } @Test void anEmptyFieldIsntFilled() { assertEquals(1, Fields.filled(List.of("Ann", ""))); } ✗ MISSED no test checks which values the negated predicate accepts in filled() ┌─ src/main/java/com/example/conditions/Fields.java:9 │ 8 │ public static long filled(List<String> values) { 9 │ return values.stream().filter(Predicate.not(String::isEmpty)).count(); │ ^^^ no longer negated 10 │ } │ = mutant - return values.stream().filter(Predicate.not(String::isEmpty)).count(); + return values.stream().filter(String::isEmpty).count(); = note: covered by FieldsTest#anEmptyFieldIsntFilled (its 1 covering test still passes) = help: use a value the predicate accepts and one it rejects, and assert the result for each
done when mutastic verify m73ac6755 samples/operator-docs passes reproduce mutastic run --only=m73ac6755 samples/operator-docs or ignore mutastic ignore m73ac6755 --reason="<why>" samples/operator-docsThe test that catches it: values it accepts and values it rejects, in numbers that differ, or a check on which come out.
@Test void onlyWrittenLinesCount() { assertEquals(2, Lines.written(List.of("total: 3", "paid", " "))); } @Test void twoFieldsOfThreeAreFilled() { assertEquals(2, Fields.filled(List.of("Ann", "", "London"))); }Edition notes: Java only: Rust’s std has no Predicate combinators, and ! inside a closure is
other comparison’s there. Run them with --operators=defaults,extended.