{"slug": "ai-in-software-testing-why-generated-tests-miss-bugs", "title": "AI in Software Testing: Why Generated Tests Miss Bugs", "summary": "A developer working on account-deletion logic found that a coding agent's generated tests passed while the implementation deleted accounts before verifying eligibility, because the tests asserted only on HTTP status codes rather than persisted state. The writeup argues that optimizing agents for coverage targets like \"increase coverage\" leaves expected behaviour underspecified, and that tests must assert on the state that must be preserved, not just the response returned.", "body_md": "AI in software testing can push coverage up while missing the bug that matters.\n\nDuring our [account-deletion work](https://nulltensor.com/posts/six-feedback-loops-ai-first-engineering-pipeline/?utm_source=devto&utm_medium=crosspost&utm_campaign=six-feedback-loops-ai-first-engineering-pipeline), a coding agent produced an implementation and passing tests. Further testing exposed a gap: a failed subscription lookup was being treated as “no active subscription”. The application allowed deletion when it could not establish whether the account was eligible.\n\nWe agreed that deletion should be blocked in that situation and added a regression test. Reviewing its assertions raised another concern. Checking that the endpoint returned an error would be insufficient if the account had already been deleted.\n\nThat distinction matters when using AI in software testing. A generated test can raise coverage while accepting the wrong behaviour, so we need to examine which incorrect behaviours could still satisfy its assertions.\n\nStatement coverage records which executable statements ran. Branch coverage adds information about which control-flow transitions were exercised. These measurements help identify code that tests have not reached. [Coverage.py’s documentation](https://coverage.readthedocs.io/en/7.14.1/branch.html) illustrates how a function can have complete statement coverage while still containing an untested branch.\n\nHowever, executing a statement does not establish that its effect was correct.\n\nA test can execute the deletion, receive an error response, and pass because its only assertion checks the response. The coverage report can accurately show that the deletion line ran. The missing piece is an assertion that rejects the resulting state.\n\nThis problem applies to human-written tests too. With AI, we need to pay particular attention to what we ask the agent to optimise. “Increase coverage” [gives it a measurable target](https://nulltensor.com/posts/agent-pr-merge-rate/?utm_source=devto&utm_medium=crosspost&utm_campaign=agent-pr-merge-rate), but leaves the expected behaviour underspecified.\n\nLet us use a small Python example to isolate the problem. This is an illustrative model of the assertion gap, rather than our production implementation.\n\nSuppose the agreed rule is that deletion requires confirmed eligibility. Unknown or ineligible accounts must remain unchanged.\n\nThe following implementation violates that rule:\n\n``` python\ndef delete_account(account, eligibility):\n    account[\"deleted\"] = True\n\n    if eligibility != \"eligible\":\n        return 409\n\n    return 204\n```\n\nDeletion happens before the eligibility check. Yet this test passes:\n\n``` python\ndef test_unknown_eligibility_returns_conflict():\n    account = {\"deleted\": False}\n\n    status = delete_account(account, \"unknown\")\n\n    assert status == 409\n```\n\nThe function returned the expected status. It also deleted the account.\n\nAdding an eligible-account test could exercise the remaining return statement. We could then execute every statement in this function while still failing to detect its central defect, provided we continued checking only response codes.\n\nThe test needs to express the preservation requirement:\n\n``` python\ndef test_unknown_eligibility_preserves_account():\n    account = {\"deleted\": False}\n\n    status = delete_account(account, \"unknown\")\n\n    assert status == 409\n    assert account[\"deleted\"] is False\n```\n\nThe additional assertion catches the premature deletion. Moving the eligibility check before the state change addresses this simplified failure.\n\nIn an application test, the equivalent check should inspect the relevant persisted state. Checking an unchanged in-memory object would be insufficient if the endpoint updated the database through another instance or query.\n\nIf we provide an implementation and ask an agent to write tests, the implementation becomes one source from which it infers expected behaviour.\n\nThat can be useful for understanding interfaces and constructing fixtures. However, the implementation may contain the very misunderstanding we want the test to expose.\n\nFor account deletion, “return an error when eligibility is unknown” leaves room for the broken example above. A more complete requirement is:\n\nWhen eligibility cannot be established, reject deletion and preserve the account.\n\nWe can give the agent that requirement alongside the code and ask it to identify the observations needed to verify it.\n\nA useful prompt is:\n\nGenerate tests for these reviewed acceptance criteria. For each test, explain which incorrect behaviour would make it fail. Include the relevant resulting state, not only the response. Flag any expected behaviour that the criteria leave unresolved.\n\nThis gives the agent a clearer task. It also makes ambiguity visible before it becomes an assertion.\n\nThe engineer still needs to review those expectations. An agent may correctly translate an incorrect business rule into executable tests. Passing them would establish agreement with that rule, while the product decision remained wrong.\n\nOnce a regression test exists, I want evidence that it detects the failure it was written for.\n\nIn our account-deletion work, we checked that the regression test failed against the broken implementation and passed after the correction. The reason for failure mattered. A missing fixture or an unrelated exception would not demonstrate that the assertion detected the eligibility problem.\n\nFor a new test without a historical defect, we can make a small, deliberate change in a local working copy. In this example, move deletion ahead of the eligibility check and rerun the test. If it still passes, inspect what it observes.\n\nWe should also test the legitimate success case. An implementation that rejects every deletion request could satisfy all the rejection tests while making the feature unusable.\n\nThese checks give us evidence about particular behaviours. They do not prove that every possible defect is detectable. A concurrency issue, for example, may require a test that controls the sequence of reads and writes across competing operations.\n\nA failing test gives an agent feedback, but “make the suite green” leaves an important decision open: whether to change the implementation or the expectation.\n\nSometimes the test is wrong. However, changing a reviewed assertion should require an explanation tied to the intended behaviour.\n\nFor a confirmed regression, I would ask the agent to preserve the agreed expectation, propose the implementation fix, and return the test result with the diff. If it believes the assertion needs to change, that disagreement should come back for review.\n\nThis keeps the feedback loop connected to the product requirement. Otherwise, the agent can remove the signal that exposed the problem.\n\nFor a lean engineering team, a practical starting point is one important failure path in an upcoming change.\n\nWrite down what must happen and what must remain unchanged. Ask AI to generate the test, then inspect whether a plausible incorrect implementation could still pass. Check the failure against a broken version and retain a legitimate success case.\n\nCoverage remains useful for finding code the suite has not exercised. Alongside it, we need evidence that the tests can distinguish acceptable behaviour from the failures we care about.\n\nThe engineering judgement is in defining that distinction. AI can help turn it into checks that run on every subsequent change.\n\n*Originally published on [nulltensor.com](https://nulltensor.com/posts/ai-generated-test-coverage/?utm_source=devto&utm_medium=crosspost&utm_campaign=ai-generated-test-coverage).*", "url": "https://wpnews.pro/news/ai-in-software-testing-why-generated-tests-miss-bugs", "canonical_source": "https://dev.to/rss_holmes/ai-in-software-testing-why-generated-tests-miss-bugs-cla", "published_at": "2026-09-26 03:50:47+00:00", "updated_at": "2026-09-26 04:00:25.416240+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "artificial-intelligence"], "entities": ["Coverage.py"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/ai-in-software-testing-why-generated-tests-miss-bugs", "markdown": "https://wpnews.pro/news/ai-in-software-testing-why-generated-tests-miss-bugs.md", "text": "https://wpnews.pro/news/ai-in-software-testing-why-generated-tests-miss-bugs.txt", "jsonld": "https://wpnews.pro/news/ai-in-software-testing-why-generated-tests-miss-bugs.jsonld"}}