MIST-RL: Mutation-based Incremental Suite Testing via Reinforcement Learning
Organizations: Fudan University, Shanghai, China · University of Science and Technology of China, Hefei, China · Xi’an Jiaotong University, Xi’an, China · South China University of Technology, Guangzhou, China
Abstract
Large Language Models (LLMs) often fail to generate correct code on the first attempt, which requires using generated unit tests as verifiers to validate the solutions. Despite the success of recent verification methods, they remain constrained by a "scaling-by-quantity" paradigm. This brute-force approach suffers from a critical limitation: it yields diminishing returns in fault detection while causing severe test redundancy. To address this, we propose MIST-RL (Mutation-based Incremental Suite Testing via Reinforcement Learning), a framework that shifts the focus to "scaling-by-utility". We formulate test generation as a sequential decision process optimized via Group Relative Policy Optimization (GRPO). Specifically, we introduce a novel incremental mutation reward combined with dynamic penalties, which incentivizes the model to discover new faults while it suppresses functionally equivalent assertions. Experiments on HumanEval+ and MBPP+ demonstrate that MIST-RL outperforms state-of-the-art baselines. It achieves a +28.5% higher mutation score while reducing the number of test cases by 19.3%. Furthermore, we show that these compact, high-utility tests serve as superior verifiers, which improves downstream code reranking accuracy on HumanEval+ by 3.05% over the SOTA baseline with 10 candidate samples. The source code and data are provided in the supplementary material.
Figures & tables
| Dataset | Model | Avg. Length | Source Pass (%) | Mutant Kill (%) |
|---|---|---|---|---|
| HumanEval+ | Llama-3-8B-Instruct | 2.33 | 40.99 | 29.18 |
| CodeRM-8B | 7.61 | 70.61 | 45.53 | |
| Qwen3-14B | 5.12 | 74.31 | 58.69 | |
| MIST-RL (Ours) | 6.14 | 74.86 | 74.03 | |
| MBPP+ | Llama-3-8B-Instruct | 2.35 | 42.51 | 22.54 |
| CodeRM-8B | 6.55 | 72.67 | 61.08 |
| Verifier Model | Reranked Pass@1 (%) | |
|---|---|---|
| Samples (N=10) | Samples (N=20) | |
| Random Selection | 22.50 | 22.50 |
| CodeRM-8B | 45.73 | 61.59 |
| Qwen3-14B | 44.51 | 55.49 |
| MIST-RL (Ours) | 48.78 | 62.80 |
| Oracle (Upper Bound) | 70.12 | 75.00 |
| Problem: move_one_ball(arr) (Check if sortable by rotation) |
| Hard Mutant: Change loop range(1, len) range(2, len) (Skips shift-by-1 check) |
| CodeRM-8B Test Case (Fails to Kill): |
| self.assertTrue(move_one_ball([3, 4, 5, 1, 2])) |
| Analysis: Requires 2 shifts. The mutant loop starts at index 2, so it still catches this. Result: Mutant Alive. |
| MIST-RL Test Case (Kills Mutant): |
| self.assertTrue(move_one_ball([2, 1])) |
Appendix figures & tables4 assets
Supplementary material from the paper’s appendix.
Appendix
| Case Study: Problem move_one_ball (HumanEval/109) | |
|---|---|
| 1. Source Code & Problem Description | 2. The ”Hard” Mutant (Off-by-One Error) |
| def move_one_ball(arr): """ Determine if it is possible to get a sorted array by performing right shift operations. Ex: [3, 4, 5, 1, 2] -> True (2 shifts) Ex: [3, 5, 4, 1, 2] -> False """ if len(arr) == 0: return True sorted_arr = sorted(arr) if arr == sorted_arr: return True # Check all possible rotations for i in range(1, len(arr)): if arr[i:] + arr[:i] == sorted_arr: return True return False | The mutation engine generates an ”Off-by-One” error by modifying the loop range. This simulates a common developer bug where the boundary condition is mishandled. Mutated Code Snippet: # Original: range(1, len(arr)) # Mutated: range(2, len(arr)) for i in range(2, len(arr)): # The loop now skips the case i=1 # (shifting by exactly 1 position). if arr[i:] + arr[:i] == sorted_arr: return True Effect: The function now correctly handles arrays needing 0 shifts or shifts, but fails for arrays needing exactly 1 shift. |
| 3. CodeRM-8B Generated Tests (Baseline) | 4. MIST-RL Generated Tests (Ours) |
| Status: FAILED to kill the mutant. CodeRM generates many tests, but they are redundant or ”lucky” (requiring 0 or shifts). def test_code_rm(self): # Case 1: Already sorted (0 shifts) # Mutant result: True (Correct) -> Alive self.assertTrue(move_one_ball([1..5])) # Case 2: Requires 2 shifts # Loop starts at 2, so it finds this! # Mutant result: True (Correct) -> Alive self.assertTrue(move_one_ball([3..2])) # Case 3: Large Random # Mutant result: Matches Original -> Alive self.assertTrue(move_one_ball([10..100])) | Status: SUCCESSFULLY killed the mutant. MIST-RL identifies the specific boundary case that requires exactly 1 shift. def test_mist_rl(self): # ... (omitted common tests) ... # CRITICAL TEST CASE FOUND BY MIST-RL # Input: [2, 1] # Sorted: [1, 2] # Requires: Exactly 1 right shift. # Under Mutant (range starts at 2): # The loop range(2, 2) is empty. # Loop is skipped -> Returns False. # Assertion Fails -> MUTANT KILLED self.assertTrue(move_one_ball([2, 1])) |
| Hyperparameter | Value |
|---|---|
| Optimization | |
| Optimizer | AdamW |
| Learning Rate | 1.5e-5 |
| LR Scheduler | Cosine |
| Warmup Ratio | 0.03 |
| Global Batch Size | 64 (2 GPUs 2 batch 16 accum) |
| Parameter | Symbol | Value |
|---|---|---|
| Quality Weight | 0.05 | |
| Marginal Utility Weight | 3.0 | |
| Base Redundancy Penalty | 0.5 | |
| Penalty Growth Rate | 1.0 | |
| Max Sequence Reference | 10 | |
| Execution Failure Penalty | (Suite) | -100.0 |
| Category | AST Node Type | Implemented Transformations |
| AOR | ast.Add (+) | Sub (-) , Mult (*) |
| (Arithmetic) | ast.Sub (-) | Add (+) , Mult (*) |
| ast.Mult (*) | Div (/) , Add (+) , Pow (**) | |
| ast.Div (/) | Mult (*) , FloorDiv (//) | |
| ast.Mod (%) | Mult (*) , Add (+) | |
| ROR | ast.Eq (==) | NotEq (!=) |
Automated Test Generation
Automated test generation creates inputs, scenarios, and checks to expose software and system defects with less manual effort.
See all 132 papers