{"title":"Review AI-Generated JUnit Tests Before They Raise Coverage","excerpt":"Two JUnit suites for the same pricing rule produce identical JaCoCo rows, and only one of them fails when the threshold or the rounding mode changes. This guide covers mapping each generated test to a clause of the ticket, BigDecimal oracle traps (the double constructor, scale-sensitive equals), why stubbing the class that owns the rule shows up as NO_COVERAGE and inflates PIT's test strength, which mutants PIT's current DEFAULTS and STRONGER groups actually generate, and how to seed by hand the RoundingMode mutant no PIT operator produces.","canonicalUrl":"https://automationtester.in/blog/ai-in-testing/review-ai-generated-junit-tests-before-coverage","category":{"name":"AI in Testing","slug":"ai-in-testing"},"tags":["junit","java","mutation-testing","pitest","code-coverage","ai-testing"],"author":{"name":"Shashank Rawlani","url":"https://shashank.rawlani.com","linkedin":"https://www.linkedin.com/in/shashankrawlani/"},"datePublished":"2026-10-03T13:30:00.000Z","dateModified":"2026-10-14T11:09:52.936Z","coverImage":{"url":"https://ik.imagekit.io/automationtester/automationtester-in/blog/covers/v3/review-ai-generated-junit-tests-before-coverage.webp","alt":"Dark technical illustration read from left to right across two side-by-side rounded lanes separated by a dashed green vertical line with a ringed dot at each end. Above each lane sits the same fully filled green gauge bar, identical in both. Inside each lane a thick green line runs level, then steps down to a lower level partway across. At each step a dashed orange segment continues along the upper level a little further before dropping, marking the same step shifted to the right. Beneath both lanes a grey axis with small ticks crosses the full width, and dashed green stems rise from ringed dots on the axis to ringed dots sitting on the green step. In the left lane the two stems are far apart, one near the start of the upper level and one near the end of the lower level, and a dashed orange line runs beneath the second and ends in an orange arrowhead at the lane's right edge. In the right lane the two stems sit tightly either side of the step; the second lands beneath the orange shifted segment, and above it a crossed-out orange ring marks that segment struck out, while a green line continues from the lane to a green arrowhead on the right."},"contentHtml":"<div class=\"callout callout-info\"><strong>Quick answer:</strong> Accept a generated JUnit test when three things hold: you can point to the clause of the requirement it defends, its expected value was worked out from that clause rather than read back from the code, and it fails when that clause is broken. A coverage report cannot check any of the three. In the pricing example below, the generated suite and the reviewed suite produce byte-identical JaCoCo rows. Run PIT against the one class under review, treat <code class=\"language-text\">NO_COVERAGE</code> and <code class=\"language-text\">SURVIVED</code> as two different defects, and seed by hand the mutants PIT has no operator for, such as a changed <code class=\"language-text\">RoundingMode</code>.</div>\n\n<p>The ticket, PRICE-212, is two sentences long: orders of 10 or more units take 5% off the line, and the line total is rounded <code class=\"language-text\">HALF_UP</code> to two decimal places. The implementation is small enough to read in one breath.</p>\n\n<pre class=\"language-java\"><code class=\"language-java\">public final class BulkDiscount {\n\n    static final int THRESHOLD = 10;\n    private static final BigDecimal MULTIPLIER = new BigDecimal(\"0.95\");\n\n    public BigDecimal apply(BigDecimal gross, int quantity) {\n        BigDecimal net = quantity &gt;= THRESHOLD ? gross.multiply(MULTIPLIER) : gross;\n        return net.setScale(2, RoundingMode.HALF_UP);\n    }\n}\n\npublic final class Checkout {\n\n    private final Catalogue catalogue;\n    private final BulkDiscount discount;\n\n    // constructor omitted\n\n    public BigDecimal lineTotal(String sku, int quantity) {\n        BigDecimal gross = catalogue.unitPrice(sku).multiply(BigDecimal.valueOf(quantity));\n        return discount.apply(gross, quantity);\n    }\n}</code></pre>\n\n<p>An assistant was asked for tests and returned these. Both classes compile, both pass, and the package reports every line and every branch covered.</p>\n\n<pre class=\"language-java\"><code class=\"language-java\">class BulkDiscountTest {\n\n    private final BulkDiscount discount = new BulkDiscount();\n\n    @Test\n    void discountsLargeOrders() {\n        BigDecimal gross = new BigDecimal(\"100.00\");\n        BigDecimal expected = gross.multiply(new BigDecimal(\"0.95\")).setScale(2, RoundingMode.HALF_UP);\n        assertEquals(expected, discount.apply(gross, 50));\n    }\n\n    @Test\n    void leavesSmallOrdersAlone() {\n        assertEquals(new BigDecimal(\"20.00\"), discount.apply(new BigDecimal(\"20.00\"), 2));\n    }\n}\n\nclass CheckoutTest {\n\n    @Test\n    void appliesBulkDiscountToLargeOrders() {\n        Catalogue catalogue = mock(Catalogue.class);\n        BulkDiscount discount = mock(BulkDiscount.class);\n        when(catalogue.unitPrice(\"SKU-1\")).thenReturn(new BigDecimal(\"2.00\"));\n        when(discount.apply(any(), anyInt())).thenReturn(new BigDecimal(\"95.00\"));\n\n        assertEquals(new BigDecimal(\"95.00\"), new Checkout(catalogue, discount).lineTotal(\"SKU-1\", 50));\n    }\n}</code></pre>\n\n<p>Now break the ticket three ways, one at a time: set <code class=\"language-text\">THRESHOLD</code> to 11, swap <code class=\"language-text\">HALF_UP</code> for <code class=\"language-text\">DOWN</code>, swap it for <code class=\"language-text\">HALF_EVEN</code>. Every build stays green. Those are the three most plausible ways to misread PRICE-212, and the suite notices none of them.</p>\n\n<div class=\"callout callout-warning\"><strong>PIT's own group table is out of date, and no group mutates a rounding mode.</strong> The table on pitest.org still lists Negate Conditionals under <code class=\"language-text\">DEFAULTS</code>. PIT 1.7.2 replaced it with the remove-conditionals mutators, and the group definition in the 1.30.0 source confirms it: <code class=\"language-text\">DEFAULTS</code> is <code class=\"language-text\">INVERT_NEGS</code>, <code class=\"language-text\">MATH</code>, <code class=\"language-text\">VOID_METHOD_CALLS</code>, <code class=\"language-text\">REMOVE_CONDITIONALS_ORDER_ELSE</code>, <code class=\"language-text\">REMOVE_CONDITIONALS_EQUAL_ELSE</code>, <code class=\"language-text\">CONDITIONALS_BOUNDARY</code>, <code class=\"language-text\">INCREMENTS</code> and the returns mutators. Versions checked on 2026-09-30: PIT 1.30.0, pitest-junit5-plugin 1.2.3, gradle-pitest-plugin 1.19.0, JaCoCo 0.8.15, JUnit 6.1.3, Mockito 5.24.0, and the Java SE 27 API documentation. Every PIT and JaCoCo result quoted below comes from running this example under Gradle 9.7.1 on JDK 21.</div>\n\n<h2 id=\"identical-jacoco-rows-for-both-suites\">Identical JaCoCo rows for a useless suite and a useful one</h2>\n\n<p>Here is the JaCoCo CSV for the package after the generated tests ran, trimmed to the columns that matter. After the review, which replaced every one of those tests, the untrimmed file is identical down to the byte.</p>\n\n<pre class=\"language-text\"><code class=\"language-text\">CLASS,INSTRUCTION_MISSED,INSTRUCTION_COVERED,BRANCH_MISSED,BRANCH_COVERED,LINE_MISSED,LINE_COVERED\nCheckout,0,24,0,0,0,6\nBulkDiscount,0,23,0,2,0,4</code></pre>\n\n<p>That is not a JaCoCo defect. Its counters are derived from bytecode, and its line rule is permissive by design: \"A source line is considered executed when at least one instruction that is assigned to this line has been executed.\" Branches are counted \"for all <code class=\"language-text\">if</code> and <code class=\"language-text\">switch</code> statements\", and exception handling does not count as a branch at all.</p>\n\n<p>The line rule matters more than it sounds. Run <code class=\"language-text\">discountsLargeOrders</code> on its own and <code class=\"language-text\">BulkDiscount</code> reports 4 of 4 lines, 1 of 2 branches. The ternary sits on one line, so a single call at quantity 50 marks the whole decision \"executed\" while one of its outcomes never ran. A line percentage cannot see that. The branch counter can, and it still cannot see that neither outcome was probed anywhere near 10.</p>\n\n<p>A coverage delta answers one question: did this code execute under test. The <a href=\"/tools/test-coverage\">test coverage calculator</a> is a quick way to show a team how a percentage moves with lines rather than with behaviour. Whether to keep a generated test is a question about the requirement, asked of each test separately.</p>\n\n<h2 id=\"give-every-generated-test-a-clause\">Give every generated test a clause from the ticket</h2>\n\n<p>Before running anything, write down which sentence of the requirement each test claims to protect, and the input that would separate a correct implementation from a plausible wrong one. For the generated suite it looks like this.</p>\n\n<table>\n<thead><tr><th>Test</th><th>Clause it claims</th><th>Separating input</th><th>Does the test use it?</th></tr></thead>\n<tbody>\n<tr><td><code class=\"language-text\">discountsLargeOrders</code></td><td>10 or more units take 5% off</td><td>quantity 10, and 9 beside it</td><td>No. It uses 50, which is discounted under a threshold of 10, 11 or 49.</td></tr>\n<tr><td><code class=\"language-text\">discountsLargeOrders</code></td><td>rounded <code class=\"language-text\">HALF_UP</code> to 2 dp</td><td>a discounted value with a 5 in the third decimal place</td><td>No. 100.00 × 0.95 is 95.0000, which needs no rounding under any mode.</td></tr>\n<tr><td><code class=\"language-text\">leavesSmallOrdersAlone</code></td><td>fewer than 10 units pay full price</td><td>quantity 9</td><td>No. It uses 2.</td></tr>\n<tr><td><code class=\"language-text\">appliesBulkDiscountToLargeOrders</code></td><td>none that it can observe</td><td>any</td><td>The discount is stubbed, so no input reaches the rule.</td></tr>\n</tbody>\n</table>\n\n<p>The last row is the one to reject outright. The other three are aimed at the right rule with the wrong numbers, which is cheaper to repair than it is to regenerate. If your test classes are already organised around behaviour rather than mirroring production classes, as <a href=\"/blog/junit-5-tests-behavior-not-classes\">the first part of this series</a> argued, the \"clause it claims\" column is mostly a matter of reading the display names.</p>\n\n<h2 id=\"expected-value-computed-by-the-code-it-checks\">An expected value computed by the code it checks</h2>\n\n<p>Look at how <code class=\"language-text\">discountsLargeOrders</code> arrives at its expectation: <code class=\"language-text\">gross.multiply(new BigDecimal(\"0.95\")).setScale(2, RoundingMode.HALF_UP)</code>. That is the implementation, transcribed. Had the implementation been written with <code class=\"language-text\">HALF_EVEN</code> by mistake, a model reading it would have put <code class=\"language-text\">HALF_EVEN</code> in the oracle too, and the test would defend the bug. The discipline for this, a table of literals typed from a source outside the codebase, was covered in <a href=\"/blog/junit-parameterized-tests-without-matrix-explosion\">part two on parameterized tests</a>. <code class=\"language-text\">BigDecimal</code> adds three traps of its own that generated oracles fall into.</p>\n\n<p>The first is the double constructor. The Javadoc says plainly that \"the results of this constructor can be somewhat unpredictable\", and in a rounding test it can erase the exact case you meant to test:</p>\n\n<pre class=\"language-java\"><code class=\"language-java\">// Wrong: 1.15 has no exact binary form. The constructor takes the double as it is,\n// 1.149999999999999911182158029987476766109466552734375, so 10 units x 0.95\n// lands just below the half and HALF_UP gives 10.92. The tie is gone.\nBigDecimal gross = new BigDecimal(1.15).multiply(BigDecimal.valueOf(10));\n\n// Right: the string constructor is exact. 11.50 x 0.95 = 10.9250, a true tie,\n// which HALF_UP takes to 10.93 and both DOWN and HALF_EVEN take to 10.92.\nBigDecimal gross = new BigDecimal(\"11.50\");</code></pre>\n\n<p>The second is equality. <code class=\"language-text\">BigDecimal.equals</code> compares scale as well as value: \"2.0 is not equal to 2.00 when compared by this method\". When a generated test fails with <code class=\"language-text\">expected: &lt;95.00&gt; but was: &lt;95.0000&gt;</code>, the tempting repair is <code class=\"language-text\">compareTo(...) == 0</code>, and it deletes half the ticket, because \"to two decimal places\" is precisely a claim about scale. In this example PIT's naked-receiver mutant, which deletes the <code class=\"language-text\">setScale</code> call outright, is killed only because the assertion uses <code class=\"language-text\">assertEquals</code>.</p>\n\n<p>The third works in your favour. JUnit's implicit conversion turns a CSV string into <code class=\"language-text\">new BigDecimal(\"...\")</code>, so a column value of <code class=\"language-text\">10.93</code> arrives with scale 2 intact. A table of string literals keeps the scale claim honest without any extra code.</p>\n\n<h2 id=\"stubbing-the-class-that-owns-the-rule\">Stubbing the class that owns the rule</h2>\n\n<p><code class=\"language-text\">CheckoutTest</code> mocks <code class=\"language-text\">BulkDiscount</code>, where the rule lives, and asserts that the value it stubbed comes back. The <code class=\"language-text\">any()</code> matcher also accepts whatever gross <code class=\"language-text\">Checkout</code> computes, so the one piece of arithmetic <code class=\"language-text\">Checkout</code> owns goes unobserved too.</p>\n\n<p>Note that <code class=\"language-text\">BulkDiscount</code> is <code class=\"language-text\">final</code>, and that did not stop the model. Mockito's documentation for mocking final types states: \"Since 5.0.0, this feature is enabled by default.\" A <code class=\"language-text\">final</code> modifier is no longer a signal to a generated test that a class was not meant to be replaced.</p>\n\n<p>PIT names this defect precisely. Run it with only <code class=\"language-text\">CheckoutTest</code> selected and all four mutants in <code class=\"language-text\">BulkDiscount.apply</code> come back <code class=\"language-text\">NO_COVERAGE</code>, which the documentation defines as \"the same as Survived except there were no tests that exercised the line of code where the mutation was created\". The run's statistics: five mutants, one killed, four with no coverage, and a test strength of 100%.</p>\n\n<p>That last figure is the trap. PIT's <code class=\"language-text\">testStrengthThreshold</code> is computed as \"killed / (killed + survived), excluding mutants where no coverage information is available\". A build gated on test strength passes this suite, because the rule the suite stubbed out has been removed from the denominator. Gate on <code class=\"language-text\">mutationThreshold</code>, which counts killed against all mutants, if the point of the gate is to catch tests that route around the code.</p>\n\n<p>Widening the operators to the <code class=\"language-text\">ALL</code> group shows the cost of <code class=\"language-text\">any()</code> as well. Four mutants on the gross line of <code class=\"language-text\">Checkout</code> survive: the <code class=\"language-text\">multiply</code> call removed, replaced by its argument, replaced by its receiver, and swapped for <code class=\"language-text\">divide</code>. The stub accepts every one of those values.</p>\n\n<p>The refactor test is the other reason to reject it. Fold <code class=\"language-text\">BulkDiscount</code> into <code class=\"language-text\">Checkout</code>, a change that alters no behaviour at all, and this test stops compiling, because its stub names a method that no longer exists. Stub the boundary you do not own, here the catalogue lookup, and let the rule run.</p>\n\n<h2 id=\"what-pit-mutates-in-bulkdiscount\">What PIT mutates in BulkDiscount, and what it never will</h2>\n\n<p>\"Mutations are performed on the byte code generated by the compiler rather than on the source files.\" That one sentence from PIT's documentation explains every result in this section.</p>\n\n<p>Under <code class=\"language-text\">DEFAULTS</code>, <code class=\"language-text\">BulkDiscount.apply</code> yields three mutants: the conditional boundary, the else-forcing remove-conditional, and a null return. <code class=\"language-text\">STRONGER</code>, which the 1.30.0 source defines as <code class=\"language-text\">DEFAULTS</code> plus <code class=\"language-text\">EXPERIMENTAL_SWITCH</code> and the two <code class=\"language-text\">_IF</code> remove-conditionals, adds one more. Against the generated suite only the boundary mutant survives.</p>\n\n<p>That boundary mutant is the threshold mutant, just phrased differently. <code class=\"language-text\">CONDITIONALS_BOUNDARY</code> rewrites <code class=\"language-text\">&gt;=</code> as <code class=\"language-text\">&gt;</code>, and for an <code class=\"language-text\">int</code> quantity, <code class=\"language-text\">quantity &gt; 10</code> is the same predicate as <code class=\"language-text\">quantity &gt;= 11</code>. The literal change from 10 to 11 does appear, reported as \"Substituted 10 with 11\", but only from <code class=\"language-text\">INLINE_CONSTS</code>, which sits in <code class=\"language-text\">ALL</code> alone. It can be mutated at all because <code class=\"language-text\">javac</code> copies the value of a <code class=\"language-text\">static final int</code> into the comparison.</p>\n\n<p>The rounding rule has no operator. <code class=\"language-text\">MATH</code> replaces \"binary arithmetic operations for either integer or floating-point arithmetic\", and <code class=\"language-text\">BigDecimal.multiply</code> is a method call, not an arithmetic instruction. The source has an <code class=\"language-text\">EXPERIMENTAL_BIG_DECIMAL</code> mutator, added in 1.7.4 and missing from the documentation page, that swaps <code class=\"language-text\">add</code> with <code class=\"language-text\">subtract</code>, <code class=\"language-text\">multiply</code> with <code class=\"language-text\">divide</code> and <code class=\"language-text\">max</code> with <code class=\"language-text\">min</code>. It never touches <code class=\"language-text\">setScale</code> or its <code class=\"language-text\">RoundingMode</code> argument. The call-removal mutators delete <code class=\"language-text\">setScale</code> entirely, which is a different and much louder defect than choosing the wrong mode. And the <code class=\"language-text\">MULTIPLIER</code> field is left alone, since PIT avoids mutating static initialiser code. Across all 13 mutants <code class=\"language-text\">ALL</code> generates for this class, not one changes how the value is rounded.</p>\n\n<p>So of the two mistakes PRICE-212 most invites, PIT hands you one for free and cannot produce the other at all.</p>\n\n<h2 id=\"seeding-the-rounding-mutant-by-hand\">Seeding the rounding mutant by hand</h2>\n\n<p>A seeded mutant is a one-line patch you apply, test against, and revert.</p>\n\n<pre class=\"language-diff\"><code class=\"language-diff\">--- a/src/main/java/com/example/pricing/BulkDiscount.java\n+++ b/src/main/java/com/example/pricing/BulkDiscount.java\n@@ -13 +13 @@\n-        return net.setScale(2, RoundingMode.HALF_UP);\n+        return net.setScale(2, RoundingMode.DOWN);</code></pre>\n\n<pre class=\"language-bash\"><code class=\"language-bash\">git apply seeded/price-212-down.diff\n./gradlew test --tests 'com.example.pricing.*'   # must fail\ngit apply -R seeded/price-212-down.diff</code></pre>\n\n<p>This is the part a model is genuinely good at. Give it the ticket text, not the implementation, and ask for the ways a competent developer could misread it, each as a one-line diff. A reasonable list for PRICE-212 is <code class=\"language-text\">DOWN</code>, <code class=\"language-text\">HALF_EVEN</code>, the threshold moved by one, and a less obvious one: rounding each unit price before multiplying by the quantity. Execution decides whether each one is killed. It cannot decide whether the per-unit reading is a mutant or an ambiguity the product owner has to settle; that call, and the check that every expected value matches the ticket's arithmetic, belong to someone who knows the domain.</p>\n\n<h2 id=\"scoping-pit-to-the-rule-under-review\">Scoping PIT to the rule under review</h2>\n\n<p>PIT does not support JUnit Jupiter out of the box; its FAQ points to the JUnit 5 plugin, which \"must be placed on the classpath of the pitest tool (<strong>not</strong> on the classpath of the project being mutated)\". The Gradle plugin PIT's documentation links to does that through one property. The results above came from this configuration:</p>\n\n<pre class=\"language-kotlin\"><code class=\"language-kotlin\">plugins {\n    java\n    id(\"info.solidsoft.pitest\") version \"1.19.0\"\n}\n\npitest {\n    pitestVersion = \"1.30.0\"\n    junit5PluginVersion = \"1.2.3\"     // adds pitest-junit5-plugin to PIT's own classpath\n    targetClasses = setOf(\"com.example.pricing.*\")\n    targetTests = setOf(\"com.example.pricing.*\")\n    mutators = setOf(\"STRONGER\")\n    outputFormats = setOf(\"XML\", \"HTML\")\n    timestampedReports = false\n}</code></pre>\n\n<p>Two details are worth knowing before the first run. The Gradle plugin reads tests from <code class=\"language-text\">sourceSets.test</code> by default, so an integration suite modelled as its own source set, as in <a href=\"/blog/gradle-jvm-test-suites-unit-integration-contract\">part four</a>, stays out of mutation runs unless you add it to <code class=\"language-text\">testSourceSets</code>. The JUnit 5 plugin has turned off Jupiter's parallel mode automatically since release 1.1.1, so a suite that only passes when run concurrently, the situation <a href=\"/blog/debug-junit-parallel-execution-flakes\">part three</a> diagnosed, will behave differently under PIT than under <code class=\"language-text\">./gradlew test</code>.</p>\n\n<p>PIT writes HTML by default, XML or CSV on request, and records one of seven outcomes per mutant: killed, survived, no coverage, non viable, timed out, memory error, run error. Asked whether results are random, the FAQ answers \"No\", naming timeouts and static initialisers as the exceptions, which is what makes a mutation result usable as review evidence. One caution: if PIT reports zero line coverage for classes your tests obviously exercise, it is not seeing your tests, and every mutant will read as no coverage. Fix the setup before reading anything else.</p>\n\n<h2 id=\"the-reviewed-pricing-tests\">The reviewed pricing tests</h2>\n\n<p>Two rows replace the generated <code class=\"language-text\">BulkDiscountTest</code>, and a real rule replaces the stub in <code class=\"language-text\">CheckoutTest</code>.</p>\n\n<pre class=\"language-java\"><code class=\"language-java\">class BulkDiscountTest {\n\n    private final BulkDiscount discount = new BulkDiscount();\n\n    // PRICE-212: \"10 or more units take 5% off the line; round the line\n    // total HALF_UP to 2 decimal places.\" Expected values worked by hand.\n    @ParameterizedTest\n    @CsvSource(useHeadersInDisplayName = true, textBlock = \"\"\"\n        QUANTITY, GROSS, EXPECTED\n        # one unit short of the threshold: no discount, nothing to round\n        9,        10.35, 10.35\n        # at the threshold: 11.50 x 0.95 = 10.925, an exact half\n        10,       11.50, 10.93\n        \"\"\")\n    void bulkRuleStartsAtTenAndRoundsHalfUp(int quantity, BigDecimal gross, BigDecimal expected) {\n        assertEquals(expected, discount.apply(gross, quantity));\n    }\n}\n\nclass CheckoutTest {\n\n    @Test\n    void pricesTheLineThroughTheRealDiscountRule() {\n        Catalogue catalogue = sku -&gt; new BigDecimal(\"1.15\");   // the boundary we do not own\n        Checkout checkout = new Checkout(catalogue, new BulkDiscount());\n\n        assertEquals(new BigDecimal(\"10.93\"), checkout.lineTotal(\"MUG-01\", 10));\n    }\n}</code></pre>\n\n<p>Quantity 10 sits exactly on the threshold, so moving the threshold to 11 removes the discount there and 11.50 comes back instead of 10.93. The same row carries a true tie at the third decimal, so <code class=\"language-text\">DOWN</code> and <code class=\"language-text\">HALF_EVEN</code> both produce 10.92. Quantity 9 pins the other side, and it is what kills a threshold moved down to 9. <code class=\"language-text\">Catalogue</code> is an interface, so a lambda stands in for it and Mockito is not needed at all.</p>\n\n<p>Text-block comments are legal here because the <code class=\"language-text\">#</code> is the first character on each line once Java strips the incidental indentation, a requirement the JUnit guide spells out. With <code class=\"language-text\">useHeadersInDisplayName</code>, the seeded <code class=\"language-text\">DOWN</code> mutant fails as <code class=\"language-text\">[2] QUANTITY = \"10\", GROSS = \"11.50\", EXPECTED = \"10.93\"</code> with <code class=\"language-text\">expected: &lt;10.93&gt; but was: &lt;10.92&gt;</code>.</p>\n\n<p>The evidence to attach to the pull request links each mutant to a clause and to the row that kills it:</p>\n\n<pre class=\"language-text\"><code class=\"language-text\">rule       PRICE-212  qty &gt;= 10 takes 5% off; line total HALF_UP to 2 dp\ncoverage   JaCoCo lines 10/10, branches 2/2 (unchanged by this review)\n\nmutant                               clause      before     after   killed by\nCONDITIONALS_BOUNDARY  apply:12      threshold   SURVIVED   KILLED  row [2] qty 10\nseeded  THRESHOLD 10 -&gt; 11           threshold   survived   killed  row [2] qty 10\nseeded  HALF_UP -&gt; DOWN              rounding    survived   killed  row [2], CheckoutTest\nseeded  HALF_UP -&gt; HALF_EVEN         rounding    survived   killed  row [2], CheckoutTest\nCheckoutTest run alone               all         NO_COVERAGE x4        real rule executes</code></pre>\n\n<p>Under <code class=\"language-text\">STRONGER</code> PIT kills all five mutants in the reviewed suite; under <code class=\"language-text\">ALL</code>, all 24.</p>\n\n<h2 id=\"before-the-next-generated-test-merges\">Before the next generated JUnit test merges</h2>\n\n<p>Pick one class in the pull request that encodes a business rule with a number in it. Fill in the four-column table for every generated test that touches it, and reject any test whose \"clause it claims\" cell is empty, starting with any test that stubs the class holding the rule.</p>\n\n<p>Then run PIT with <code class=\"language-text\">targetClasses</code> narrowed to that class and <code class=\"language-text\">STRONGER</code> selected. Read no-coverage results first, because each one is a rule no test reached, and a test-strength figure will hide them. Read survivors second. For every clause PIT has no operator for, ask a model for one-line misreadings of the ticket, apply each, and confirm the build fails.</p>\n\n<p>The artefact to capture is the clause-to-mutant table above, committed with the tests or pasted into the review. The coverage figure can go in too, labelled as what it is: evidence that the code ran.</p>\n\n<h2 id=\"questions-about-accepting-generated-jvm-tests\">Questions about accepting generated JVM tests</h2>\n\n<h3 id=\"should-the-build-enforce-a-mutation-score\">Should the build enforce a mutation score?</h3>\n\n<p>On a package you have already reviewed, yes, and use <code class=\"language-text\">mutationThreshold</code> rather than <code class=\"language-text\">testStrengthThreshold</code> for the reason shown above. Mind the rounding as well. Thresholds are integer percentages by default, and PIT's Maven documentation spells out the resulting blind spot of roughly one percentage point; <code class=\"language-text\">thresholdPrecision</code>, added in 1.25.4, lets you compare at one or more decimal places.</p>\n\n<h3 id=\"is-a-mocked-collaborator-in-a-generated-test-always-wrong\">Is a mocked collaborator in a generated test always wrong?</h3>\n\n<p>No. Mocking a boundary the class does not own, such as a catalogue service, a clock or a payment gateway, is what makes a unit test deterministic. The defect is stubbing the collaborator that computes the outcome being asserted. The quick check: if deleting the production class under test would leave the test green, the mock is in the wrong place. PIT reports exactly that case as no coverage.</p>\n\n<h3 id=\"can-the-model-write-expected-values-from-the-ticket\">Can the model write the expected values if it is given the ticket instead of the code?</h3>\n\n<p>It can propose them, which beats transcribing the implementation. A person still redoes the arithmetic, because a hand-worked oracle is only worth having if it comes from outside the chain that produced the code. Checking 11.50 × 0.95 = 10.925 takes seconds, and that check is the review.</p>\n\n<h3 id=\"does-this-work-for-kotlin-code\">Does this work for Kotlin code on the JVM?</h3>\n\n<p>Not with stock PIT. Its FAQ warns that bytecode from languages it does not explicitly support yields \"junk\" mutants, and lists Kotlin as supported only through the Arcmutate Kotlin plugin. The review table and hand-seeded mutants work in any language.</p>\n\n<h2 id=\"ai-junit-review-references\">Primary references</h2>\n\n<ul>\n<li><a href=\"https://pitest.org/quickstart/mutators/\">PIT — Mutation operators</a>. That mutations \"are performed on the byte code generated by the compiler rather than on the source files\"; the <code class=\"language-text\">CONDITIONALS_BOUNDARY</code> replacement table (<code class=\"language-text\">&gt;=</code> to <code class=\"language-text\">&gt;</code>); that <code class=\"language-text\">MATH</code> covers \"binary arithmetic operations for either integer or floating-point arithmetic\"; <code class=\"language-text\">INLINE_CONSTS</code>, <code class=\"language-text\">NON_VOID_METHOD_CALLS</code>, the experimental naked-receiver and argument-propagation mutators; and the group table that still places Negate Conditionals in <code class=\"language-text\">DEFAULTS</code>.</li>\n<li><a href=\"https://github.com/hcoles/pitest/blob/1.30.0/pitest/src/main/java/org/pitest/mutationtest/engine/gregor/config/StandardMutatorGroups.java\">PIT 1.30.0 source — StandardMutatorGroups.java</a>. The actual membership of <code class=\"language-text\">DEFAULTS</code> and <code class=\"language-text\">STRONGER</code> at the current release, which differs from the documentation table.</li>\n<li><a href=\"https://github.com/hcoles/pitest/blob/1.30.0/pitest/src/main/java/org/pitest/mutationtest/engine/gregor/mutators/experimental/BigDecimalMutator.java\">PIT 1.30.0 source — BigDecimalMutator.java</a>. That <code class=\"language-text\">EXPERIMENTAL_BIG_DECIMAL</code> swaps <code class=\"language-text\">add</code>/<code class=\"language-text\">subtract</code>, <code class=\"language-text\">multiply</code>/<code class=\"language-text\">divide</code>, <code class=\"language-text\">max</code>/<code class=\"language-text\">min</code> and <code class=\"language-text\">negate</code>/<code class=\"language-text\">plus</code>, and has no replacement for <code class=\"language-text\">setScale</code> or rounding modes.</li>\n<li><a href=\"https://github.com/hcoles/pitest/blob/1.30.0/README.md\">PIT README at 1.30.0 — release notes</a>. 1.7.2 \"Change default mutators - replace negate conditional with remove conditional\", 1.7.4 adding the BigDecimal mutator, and 1.25.4 adding configurable decimal precision. The <a href=\"https://github.com/hcoles/pitest/releases/tag/1.30.0\">1.30.0 release page</a> explains the jump from 1.25.9.</li>\n<li><a href=\"https://pitest.org/quickstart/basic_concepts/\">PIT — Basic concepts</a>. The seven mutant outcomes, and that \"no coverage is the same as Survived except there were no tests that exercised the line of code where the mutation was created\".</li>\n<li><a href=\"https://pitest.org/quickstart/maven/\">PIT — Maven quick start</a>. <code class=\"language-text\">targetClasses</code> and <code class=\"language-text\">targetTests</code> as globs, <code class=\"language-text\">outputFormats</code> (HTML, XML, CSV; HTML by default), <code class=\"language-text\">mutationThreshold</code>, <code class=\"language-text\">testStrengthThreshold</code> defined as \"killed / (killed + survived), excluding mutants where no coverage information is available\", and the integer threshold blind spot with <code class=\"language-text\">thresholdPrecision</code>.</li>\n<li><a href=\"https://pitest.org/faq/\">PIT — FAQ</a>. That JUnit 5 needs a plugin, that PIT avoids mutating static initialisers, the <code class=\"language-text\">DEFAULTS</code>/<code class=\"language-text\">STRONGER</code>/<code class=\"language-text\">ALL</code> groups with <code class=\"language-text\">ALL</code> \"strongly discouraged\", that results are deterministic apart from timeouts and static initialisers, the advice to limit analysis to changed code, and Kotlin support only via Arcmutate.</li>\n<li><a href=\"https://github.com/pitest/pitest-junit5-plugin\">pitest-junit5-plugin — README</a>. Release 1.2.3, that the plugin must sit on PIT's classpath rather than the project's, and that 1.1.1 automatically disables parallel mode.</li>\n<li><a href=\"https://github.com/szpak/gradle-pitest-plugin\">gradle-pitest-plugin — README</a>. The <code class=\"language-text\">info.solidsoft.pitest</code> plugin at 1.19.0, <code class=\"language-text\">junit5PluginVersion</code>, <code class=\"language-text\">pitestVersion</code>, the Kotlin DSL property assignment form, and <code class=\"language-text\">testSourceSets</code> defaulting to <code class=\"language-text\">sourceSets.test</code>.</li>\n<li><a href=\"https://www.eclemma.org/jacoco/trunk/doc/counters.html\">JaCoCo — Coverage Counters</a>. That counters derive from bytecode, the line rule \"a source line is considered executed when at least one instruction that is assigned to this line has been executed\", branch counting for <code class=\"language-text\">if</code> and <code class=\"language-text\">switch</code>, and that exception handling is not a branch. <a href=\"https://github.com/jacoco/jacoco/releases/tag/v0.8.15\">JaCoCo 0.8.15</a> is the current release.</li>\n<li><a href=\"https://docs.oracle.com/en/java/javase/27/docs/api/java.base/java/math/BigDecimal.html\">Java SE 27 — BigDecimal</a>. That <code class=\"language-text\">equals</code> considers scale, so \"2.0 is not equal to 2.00\"; that the double constructor's results \"can be somewhat unpredictable\"; and <code class=\"language-text\">setScale(int, RoundingMode)</code> applying the rounding mode when the scale is reduced.</li>\n<li><a href=\"https://docs.oracle.com/en/java/javase/27/docs/api/java.base/java/math/RoundingMode.html\">Java SE 27 — RoundingMode</a>. <code class=\"language-text\">HALF_UP</code> rounding up when \"both neighbors are equidistant\", <code class=\"language-text\">HALF_EVEN</code> behaving as <code class=\"language-text\">HALF_UP</code> only when the digit left of the discarded fraction is odd, and <code class=\"language-text\">DOWN</code> never incrementing the retained digit.</li>\n<li><a href=\"https://javadoc.io/doc/org.mockito/mockito-core/5.24.0/org.mockito/org/mockito/Mockito.html\">Mockito 5.24.0 — Mockito class Javadoc</a>. Section 39: mocking final types and methods, \"Since 5.0.0, this feature is enabled by default\"; and the argument-matcher behaviour of <code class=\"language-text\">any()</code> and <code class=\"language-text\">anyInt()</code>.</li>\n<li><a href=\"https://docs.junit.org/6.1.3/writing-tests/parameterized-classes-and-tests.html\">Parameterized classes and tests, in the JUnit 6.1.3 User Guide</a>. <code class=\"language-text\">@CsvSource</code> with <code class=\"language-text\">textBlock</code> and <code class=\"language-text\">useHeadersInDisplayName</code>, the display-name format, comment lines requiring <code class=\"language-text\">#</code> as the first character, and implicit conversion of a string to <code class=\"language-text\">new BigDecimal(...)</code>.</li>\n</ul>\n"}