Skip to content
mutastic

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.

OperatorChangesSet
mutastic/boundary< and <=, > and >=defaults
mutastic/comparisona comparison to the one that differs on one side: < to !=stronger
mutastic/other-comparisona comparison to the others, and a condition to its oppositestronger
mutastic/remove-conditionala condition to always true, or always falsedefaults
mutastic/logical-connector&& and ||defaults
predicate/swap-connectorPredicate.and and orextended
predicate/remove-negatea predicate’s negate(), removedextended
predicate/remove-notPredicate.not(p) to pextended

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/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));
}
Terminal window
✗ 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-docs

The 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.

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:

WrittenBecomesDiffers where
a < ba != ba > b
a <= ba == ba < b
a > ba != ba < b
a >= ba == ba > b
a == ba <= b, and a >= ba < b, and a > b
a != ba < b, and a > ba > 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));
}
Terminal window
✗ 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-docs

The 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.

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());
}
Terminal window
✗ 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-docs

The 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.

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));
}
Terminal window
✗ 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-docs

The 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.

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:

Terminal window
✗ 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, mf006c884

The 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/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"));
}
Terminal window
✗ 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-docs

The 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/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", " ")));
}
Terminal window
✗ 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-docs

Predicate.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", "")));
}
Terminal window
✗ 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-docs

The 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.