<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Oleg Malyshev</title>
    <description>The latest articles on DEV Community by Oleg Malyshev (@tanki_sleva).</description>
    <link>https://dev.to/tanki_sleva</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4049976%2F289c50d1-ac60-43c0-b585-2e91b30fdd84.jpg</url>
      <title>DEV Community: Oleg Malyshev</title>
      <link>https://dev.to/tanki_sleva</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tanki_sleva"/>
    <language>en</language>
    <item>
      <title>How We Taught AI to Triage Failed Autotests and File Bugs in the Tracker</title>
      <dc:creator>Oleg Malyshev</dc:creator>
      <pubDate>Mon, 10 Aug 2026 20:28:13 +0000</pubDate>
      <link>https://dev.to/tanki_sleva/how-we-taught-ai-to-triage-failed-autotests-and-file-bugs-in-the-tracker-1mpe</link>
      <guid>https://dev.to/tanki_sleva/how-we-taught-ai-to-triage-failed-autotests-and-file-bugs-in-the-tracker-1mpe</guid>
      <description>&lt;p&gt;Hi, I'm Oleg. In the &lt;a href="https://dev.to/tanki_sleva/how-we-turned-swagger-from-documentation-into-the-engine-behind-our-api-tests-3lme"&gt;previous article&lt;/a&gt; I covered how we generate autotests from Swagger and test cases using OpenAPI Generator plus Cursor AI / Claude Code, and how we measure coverage automatically with Swagger Coverage.&lt;/p&gt;

&lt;p&gt;This one is about the other end of the pipeline: how we triage &lt;em&gt;failed&lt;/em&gt; autotests using Allure TestOps integrated with an issue tracker, an MCP server for TestOps, an MCP server for the tracker, and Cursor AI / Claude Code.&lt;/p&gt;

&lt;p&gt;But I won't start with AI. First the process itself: why we need defects in TestOps, how we triage a run by hand, why it quickly becomes drudgery without matcher rules, and what exactly we automated afterwards.&lt;/p&gt;

&lt;p&gt;Two notes for readers outside our stack: &lt;strong&gt;Allure TestOps&lt;/strong&gt; is a test management system that ties manual test cases to automated tests, CI runs, and reporting. &lt;strong&gt;Yandex Tracker&lt;/strong&gt; is an issue tracker, roughly analogous to Jira; TestOps integrates with it the same way it integrates with Jira, so if you're on a different tracker, the shape of this stays the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: the tests failed — now what?
&lt;/h2&gt;

&lt;p&gt;When autotests fail, the mere fact of a red run tells you almost nothing. You need to work out what actually happened:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a real bug in the product;&lt;/li&gt;
&lt;li&gt;the autotest itself broke;&lt;/li&gt;
&lt;li&gt;the test data went stale;&lt;/li&gt;
&lt;li&gt;the environment or infrastructure went down;&lt;/li&gt;
&lt;li&gt;the contract changed but Swagger or the tests weren't updated;&lt;/li&gt;
&lt;li&gt;or it's a known problem that already has a ticket in progress.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If all you ever look at is the list of failed/broken tests, the team quickly starts spending time re-triaging the same failures. Today a QA engineer works out that three tests fail because of one backend error; tomorrow another QA engineer opens the same three tests and walks the same path from scratch.&lt;/p&gt;

&lt;p&gt;So after a test run we have one simple rule: &lt;strong&gt;every failed result must be triaged and linked to a defect.&lt;/strong&gt; Ideally, after triage the run has no unresolved failed/broken results left.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why defects in TestOps
&lt;/h2&gt;

&lt;p&gt;A defect in TestOps isn't just another entity sitting next to a test case and a run. It's how you turn a chaotic list of failures into a usable database of &lt;em&gt;causes&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;One defect should describe one cause of failure. Not "404", "500" or "AssertionError", but a specific problem — for example, the image-fetching endpoint started returning 404 for a group of scenarios where the contract says 200.&lt;/p&gt;

&lt;p&gt;That buys several practical things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fewer duplicates.&lt;/strong&gt; Before creating a new defect, QA checks whether an open defect with the same cause already exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster repeat runs.&lt;/strong&gt; If the same error recurs in the next run, TestOps can link the failure to the existing defect automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clearer release risk.&lt;/strong&gt; Before a release you can see which critical defects are still open and which failures are already known and not blocking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A link to development.&lt;/strong&gt; When the cause is a product bug, the defect is tied to a tracker issue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easier to separate concerns.&lt;/strong&gt; Product bugs don't get mixed up with test data, environment, or authorization problems, or with bugs in the autotests themselves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main point: defects stop you re-triaging the same failure after every single run.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the manual process looks like
&lt;/h2&gt;

&lt;p&gt;The triage procedure is fairly straightforward.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Open the run and look at the unresolved results
&lt;/h3&gt;

&lt;p&gt;After the autotests finish, go to the run results in TestOps and open the overview. Unresolved results in a run mean either that no defects have been filed for the failed tests, or that defects exist but specific test results aren't linked to them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgkanklq71hyrv8ri7bmk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgkanklq71hyrv8ri7bmk.png" alt="The run list already shows there are failed/broken results that need triage." width="800" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Furl6lu2gf6ddmz33qkgp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Furl6lu2gf6ddmz33qkgp.png" alt="The run overview shows how many results are still unresolved" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One important constraint:&lt;/strong&gt; you can only create a defect and link it to a failed test in a run that isn't closed yet. So triage the failures before closing the launch.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Analyse the error in the failed test
&lt;/h3&gt;

&lt;p&gt;Next, open the failed test and look at the reason. For example, the test might fail with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Expected status code &amp;lt;200&amp;gt; but was &amp;lt;404&amp;gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmx3ytm41g9kz1xmugofw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmx3ytm41g9kz1xmugofw.png" alt="Start with the actual error: what status or exception did the autotest get." width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The error text alone usually isn't enough. You need to expand the stack trace and find the line that belongs to your autotest — the class and method where the failure happened.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ix0o5r2ryehtiq124rj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ix0o5r2ryehtiq124rj.png" alt="Caption: When the short message isn't enough, expand the full stack trace and look for the test class and method you can later use to narrow the matcher." width="800" height="786"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb2hjpmde5j1gydy350cl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb2hjpmde5j1gydy350cl.png" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;IntegrationsRegressTests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getIntegrationsProductsAllImagesWithLimitTest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we have two important pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the error text: &lt;code&gt;Expected status code &amp;lt;200&amp;gt; but was &amp;lt;404&amp;gt;.&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the failure location in the test code: &lt;code&gt;IntegrationsRegressTests.getIntegrationsProductsAllImagesWithLimitTest&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After that you need to determine the cause. Not every failed autotest equals a product bug. Sometimes the problem is the host, a token, test data, setup, cleanup, the Swagger contract, or local deserialization of the response into a generated model.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Create a new defect or reuse an existing one
&lt;/h3&gt;

&lt;p&gt;If a suitable open defect already exists, link the failed test to it. But don't stop at a plain manual link — linking the current test result to a defect only works for the current run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzr3me6w5ln03pcxgdfnb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzr3me6w5ln03pcxgdfnb.png" alt="From the failed test card you can link the result to an existing defect or create a new one." width="800" height="597"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9emp706hz85jhkyj8ccj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9emp706hz85jhkyj8ccj.png" alt="First check whether an open defect with the same cause already exists." width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1qgtu77djaoesrq4bdrh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1qgtu77djaoesrq4bdrh.png" alt="Even when picking an existing defect, still check or create the automation rule." width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For TestOps to link similar failures itself in later runs, the defect needs an automation rule.&lt;/p&gt;

&lt;p&gt;If this cause hasn't come up before, create a new defect. The title doesn't need to be elaborate at this first step, but it should convey the essence of the error. The description can then be fleshed out so a developer understands it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftu77ubnlvc59y1xrjnib.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftu77ubnlvc59y1xrjnib.png" alt="When no suitable defect exists, create a new one and articulate the cause of failure." width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4yl9o4s1sfd1i7glvzer.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4yl9o4s1sfd1i7glvzer.png" alt="The description records what failed and why it looks like a single cause." width="800" height="486"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which endpoint or scenario is failing;&lt;/li&gt;
&lt;li&gt;what request was made;&lt;/li&gt;
&lt;li&gt;what response was expected;&lt;/li&gt;
&lt;li&gt;what response came back;&lt;/li&gt;
&lt;li&gt;why this looks like a product bug rather than a test problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Create or link a tracker issue
&lt;/h3&gt;

&lt;p&gt;When a defect corresponds to a product bug, it should have a tracker issue attached. You can do this straight from TestOps: either link the defect to an existing issue, or create a new one through the bug tracker integration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr4xmnfa69twrqz66yjtu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr4xmnfa69twrqz66yjtu.png" alt="A tracker issue can be created straight from the defect through the TestOps integration." width="800" height="665"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This part matters: the issue should be created &lt;em&gt;through&lt;/em&gt; the TestOps integration, so the link between run, defect, and tracker issue stays transparent.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Configure the automation rule
&lt;/h3&gt;

&lt;p&gt;The automation rule is what makes defects actually save time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqu78y9hxygpahx7ljf42.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqu78y9hxygpahx7ljf42.png" alt="Caption: After creating or selecting a defect, always configure the automation rule." width="800" height="692"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the simple case, you put the error text in the matcher:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Expected status code &amp;lt;200&amp;gt; but was &amp;lt;404&amp;gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl7z6ail4q0r7ajo1319h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl7z6ail4q0r7ajo1319h.png" alt="A matcher on the error message finds failures with the same text." width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But that pattern can be far too broad. A 404 can show up in different places for different reasons. So it's better to constrain the rule with the stack trace as well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;IntegrationsRegressTests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getIntegrationsProductsAllImages&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk4iu73qjhjba2cw8a9ab.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk4iu73qjhjba2cw8a9ab.png" alt="Constraining by stack trace links only the intended group of failures to the defect." width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A rule like that tells TestOps: if a new run hits "expected 200, got 404" &lt;em&gt;and&lt;/em&gt; it happened in the group of tests about fetching images, link that failure to this defect automatically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmf9p6o6382qafpjktlgp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmf9p6o6382qafpjktlgp.png" alt="Before saving the rule, check which results it matched." width="799" height="136"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This doesn't just catch any 404. It catches one specific recurring cause.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F582io1900pbdlsmwwrje.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F582io1900pbdlsmwwrje.png" alt="Keep the rule name short but meaningful, so it's obvious which cause it catches." width="800" height="685"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The matcher doesn't always need to go down to a specific test method. If the problem applies to a whole test class, constraining by the class in the stack trace is enough.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxz9hb9393d21i4l9xkcu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxz9hb9393d21i4l9xkcu.png" alt="A class-level matcher: use it when one cause shows up across different methods of the same test class." width="800" height="189"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And if the error is unique and specific enough on its own, a matcher on just the error message sometimes suffices.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fijprdhxmcmxpet1otozq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fijprdhxmcmxpet1otozq.png" alt="A message-only matcher: fits unique infrastructure or system errors." width="800" height="237"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Verify the result
&lt;/h3&gt;

&lt;p&gt;After creating a defect, check that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the defect is linked to the right failed test results;&lt;/li&gt;
&lt;li&gt;the defect has an automation rule;&lt;/li&gt;
&lt;li&gt;if it's a product bug, a tracker issue is created or linked;&lt;/li&gt;
&lt;li&gt;the run overview shows fewer unresolved results;&lt;/li&gt;
&lt;li&gt;the matcher isn't so broad that it will hide new, independent errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the next run, if these same tests fail — or new tests fail with the same cause — TestOps links them to the defect automatically. And when the tracker issue is closed, the defect in TestOps can close automatically through the integration too.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgqoav3rwuf4eczczrxgq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgqoav3rwuf4eczczrxgq.png" alt="The linked defect now appears on the failed test card." width="800" height="491"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj16a7o9bcsvv2qr1v37e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj16a7o9bcsvv2qr1v37e.png" alt="On the defect card, check the matcher, the linked failed tests, and the tracker issue." width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhwxgl0wuaqlftzah0saw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhwxgl0wuaqlftzah0saw.png" alt="The tracker issue appears with links back to the test result and the defect." width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv1hcia4lvs1nij8rvupc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv1hcia4lvs1nij8rvupc.png" alt="Once failures are linked to a defect, the number of unresolved results in the run goes down." width="799" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules we settled on
&lt;/h2&gt;

&lt;p&gt;To keep the defect database from turning into a landfill, we stick to a few rules.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One defect — one cause of failure.&lt;/li&gt;
&lt;li&gt;Before creating a new defect, check whether a suitable open one already exists.&lt;/li&gt;
&lt;li&gt;Don't merge a product bug, an infrastructure problem, a test data problem, and an autotest bug into a single defect.&lt;/li&gt;
&lt;li&gt;Every new defect gets an automation rule.&lt;/li&gt;
&lt;li&gt;The matcher must not be too broad: a bare &lt;code&gt;404&lt;/code&gt;, &lt;code&gt;500&lt;/code&gt;, or &lt;code&gt;AssertionError&lt;/code&gt; with no stack trace constraint is almost always a bad idea.&lt;/li&gt;
&lt;li&gt;Only close a defect once the fix is confirmed and a fresh run has no failures from the same cause.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Classifying failures matters separately. If the cause is the autotest, the test data, authorization, the environment, or a Swagger mismatch with no backend bug behind it — don't file a product defect in the tracker. Otherwise the development team gets noise, and trust in the autotests drops.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI comes in
&lt;/h2&gt;

&lt;p&gt;The manual process works, but it still takes time. Open the run, get all failed/broken results, look at the details, group similar errors, check existing defects, create a matcher, link test results, create or verify a tracker issue, and make sure every field is filled in correctly.&lt;/p&gt;

&lt;p&gt;So to speed triage up, we wired in AI plus MCP.&lt;/p&gt;

&lt;p&gt;I've written about our MCP server for TestOps before. In short, MCP lets Cursor / Claude Code reach into TestOps, fetch the entities it needs, and write results back. For this task we use the TestOps MCP together with the tracker's MCP.&lt;/p&gt;

&lt;p&gt;On top of that I wrote a skill, &lt;code&gt;/testops-run-defects&lt;/code&gt;. It takes three parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/testops-run-defects &amp;lt;link to the launch/run in TestOps&amp;gt; &amp;lt;team&amp;gt; &amp;lt;component&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/testops-run-defects https://allure.example.com/launch/67737 red loylt-gifts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent then does roughly what a QA engineer following the procedure would do, only faster and more systematically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;extracts the launch id from the link;&lt;/li&gt;
&lt;li&gt;fetches the unresolved failed/broken test results from TestOps;&lt;/li&gt;
&lt;li&gt;requests failure details, pulling the stack trace only where it's needed to build a sane matcher;&lt;/li&gt;
&lt;li&gt;groups failures by shared cause;&lt;/li&gt;
&lt;li&gt;checks open defects and their matcher rules so it doesn't create duplicates;&lt;/li&gt;
&lt;li&gt;separates product bugs from test, data, environment, authorization, and Swagger-mismatch problems;&lt;/li&gt;
&lt;li&gt;creates defects in TestOps with matcher rules;&lt;/li&gt;
&lt;li&gt;creates a linked tracker issue through the TestOps integration;&lt;/li&gt;
&lt;li&gt;explicitly links every failed test result to its defect;&lt;/li&gt;
&lt;li&gt;via the tracker's MCP, verifies and fills in the issue fields: team, component, type, priority, and tag;&lt;/li&gt;
&lt;li&gt;finishes with a control pass: matchers exist, test results are linked, tracker issues are created, no unresolved results remain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Importantly, the AI must not just file defects for everything red. The skill has guardrails against false defects: if the cause looks like a test data problem, a wrong host, bad authorization, a broken setup, or a bug in the autotest, that result must not become a backend bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like in practice
&lt;/h2&gt;

&lt;p&gt;Below is an example run of the skill. We passed the launch link, the team &lt;code&gt;red&lt;/code&gt;, and the component &lt;code&gt;loylt-gifts&lt;/code&gt;. The agent fetched the list of failures, triaged them, created defects, linked them to tracker issues, and verified the result.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi472frgiblw0tz8ethfc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi472frgiblw0tz8ethfc.png" alt="Running the /testops-run-defects skill: launch link, team, and component." width="800" height="258"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The output is a short report: how many results were processed, which defects were created, which tracker issues were linked, and whether any unresolved failures remain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbae9plmtebbvscarfwg1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbae9plmtebbvscarfwg1.png" alt="Triage summary: 29 failed/broken results processed, defects 1279–1287 created, issues CMA-9551 – CMA-9559 filed, no unresolved results left." width="800" height="576"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this example the result was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;failed/broken unresolved processed: &lt;code&gt;29&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;defects created: &lt;code&gt;1279–1287&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;issues created and linked: &lt;code&gt;CMA-9551 – CMA-9559&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;unresolved results remaining: &lt;code&gt;0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;every defect has a matcher&lt;/li&gt;
&lt;li&gt;all 29 test results are linked to their defects&lt;/li&gt;
&lt;li&gt;the tracker issues got the right fields: team, component, type, priority, and the &lt;code&gt;autoapi&lt;/code&gt; tag&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Right now I'm actively extending the skill so it fits different teams and different components, rather than one hard-coded mapping.&lt;/p&gt;

&lt;p&gt;The next logical step is an AI agent that walks the failed autotests of every team, triages the results, files defects and issues, configures matcher rules, and leaves only the ambiguous cases to a human.&lt;/p&gt;

&lt;p&gt;The ideal picture for me looks like this: the autotests fail, the agent triages the run on its own, groups the causes, separates product bugs from test and data problems, creates the necessary defects and tracker issues — and in the morning QA sees not a heap of red tests, but a clear list of causes and actions.&lt;/p&gt;

&lt;p&gt;You can't remove the human from this process entirely. But removing most of the drudgery is well within reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Short version
&lt;/h2&gt;

&lt;p&gt;Defects in TestOps aren't a formality. They make the failure database manageable, keep duplicates down, and stop you re-triaging the same errors after every run.&lt;/p&gt;

&lt;p&gt;Matcher rules turn a defect from a manual annotation into a mechanism for automatically linking recurring failures. Tracker integration connects the autotests to development. And MCP plus Cursor AI / Claude Code automate most of the drudgery around failed results.&lt;/p&gt;

&lt;p&gt;In practice that means QA spends less time shuttling information between systems and more time on the thing that matters: working out where the product is genuinely broken, and where the tests, the data, or the infrastructure need fixing instead.&lt;/p&gt;




&lt;p&gt;One caveat worth flagging, since it came up when I published this in Russian: the "one defect — one cause" rule runs into the granularity of what the test actually reports. A single assertion covering ten malformed fields will only ever be split as finely as the report shows it. A soft assertion emitting a message per field can be decomposed properly. Worth knowing before you assume the matcher will do all the work for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let’s Work Together
&lt;/h2&gt;

&lt;p&gt;I’m open to QA leadership opportunities and collaborations with teams looking to improve their testing strategy, build scalable test automation, introduce AI-assisted QA workflows, and establish meaningful quality metrics.&lt;/p&gt;

&lt;p&gt;If this sounds relevant to your team, feel free &lt;a href="https://www.linkedin.com/in/oleg-malyshev/" rel="noopener noreferrer"&gt;to connect with me on LinkedIn.&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How We Turned Swagger from Documentation into the Engine Behind Our API Tests</title>
      <dc:creator>Oleg Malyshev</dc:creator>
      <pubDate>Sun, 09 Aug 2026 18:31:53 +0000</pubDate>
      <link>https://dev.to/tanki_sleva/how-we-turned-swagger-from-documentation-into-the-engine-behind-our-api-tests-3lme</link>
      <guid>https://dev.to/tanki_sleva/how-we-turned-swagger-from-documentation-into-the-engine-behind-our-api-tests-3lme</guid>
      <description>&lt;p&gt;Hi! I’m Oleg, a QA Engineering Manager focused on test strategy, automation, and AI-assisted QA workflows.&lt;/p&gt;

&lt;p&gt;This is a continuation of a series on where AI actually earns its place in QA — and this time the answer starts somewhere less glamorous than AI: with the OpenAPI spec.&lt;/p&gt;

&lt;p&gt;The short version: we generate a Java API client from Swagger, write tests against that generated client, measure coverage against the spec automatically in GitLab CI, and hand the resulting gaps to an LLM as a structured JSON report so it can write the missing tests.&lt;br&gt;
The problem: tests that drift away from the contract&lt;br&gt;
A very common failure mode in API testing is that tests live separately from the API contract.&lt;/p&gt;

&lt;p&gt;A QA engineer assembles URLs by hand, writes JSON by hand, parses responses by hand. Swagger exists somewhere nearby — analysts and developers keep it updated, the tests nominally check the same API — but there is no actual link between them. Nothing breaks when they diverge. You find out later, usually in production.&lt;/p&gt;

&lt;p&gt;We decided to make Swagger the source of truth for API tests rather than documentation nobody reads.&lt;/p&gt;

&lt;p&gt;Using our Cohorts service as the running example, the pipeline looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cohorts.swagger.yaml

        ↓

OpenAPI Generator + Mustache templates

        ↓

cohorts.api.CohortsApi

cohorts.model.*

        ↓

JUnit + RestAssured tests

        ↓

Swagger Coverage

        ↓

HTML / JSON coverage report

        ↓

LLM skills that generate the missing tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tests don't call given().get("/api/..."). They call a generated client with generated models, both produced from the OpenAPI spec.&lt;br&gt;
How Swagger becomes a Java client&lt;br&gt;
Take the endpoint that creates a cohort: POST /api/v1/cohort.&lt;/p&gt;

&lt;p&gt;The OpenAPI file describes it at two levels. The first is paths — which endpoint, which HTTP method, the operationId, the request schema to send and the response schema to expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/api/v1/cohort:

  post:

    tags:

      - cohorts

    summary: Create an empty cohort

    operationId: createCohort

    requestBody:

      content:

        application/json:

          schema:

            $ref: '#/components/schemas/CreateCohortsRq'

    responses:

      201:

        description: Success

        content:

          application/json:

            schema:

              $ref: '#/components/schemas/CreateCohortsRs'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second is components.schemas — the fields that make up the DTOs referenced above via $ref:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CreateCohortsRq:

  type: object

  required:

    - name

    - description

  properties:

    name:

      type: string

      description: Cohort name

    description:

      type: string

      description: Cohort description

    case_type:

      type: integer

      description: Cohort use case type

CreateCohortsRs:

  type: object

  required:

    - id

  properties:

    id:

      type: integer

      description: Cohort ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After generation, that becomes Java:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;operationId: createCohort → cohortsDefaultApi().createCohort()

CreateCohortsRq           → cohorts.model.CreateCohortsRq

CreateCohortsRs           → cohorts.model.CreateCohortsRs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Swagger has a name field, the Java model has a builder method .name(...). If Swagger has id, the response model has getId().&lt;/p&gt;

&lt;p&gt;This is the part that matters: QA doesn't invent the DTO. The test can only work with what the contract describes.&lt;br&gt;
Why we use Mustache templates&lt;br&gt;
OpenAPI Generator works fine out of the box — the standard templates will produce API classes, models, methods for path/query/body parameters, and a basic client wrapper.&lt;/p&gt;

&lt;p&gt;But when the generated code doesn't quite match your project's style, you can customise it with Mustache templates. This is a standard OpenAPI Generator mechanism: take the default template and change how the generated API classes or models look.&lt;/p&gt;

&lt;p&gt;We don't use Mustache because generation is impossible without it. We use it because we want the generated code to sit comfortably inside our existing test framework.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/test/resources/openapi-templates/api.mustache

src/test/resources/openapi-templates/pojo.mustache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They're wired into the Gradle generation task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;templateDir.set("$rootDir/src/test/resources/openapi-templates")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;api.mustache controls the generation of API classes — for example cohorts.api.CohortsApi. It's what defines the shape of the generated methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public CreateCohortOper createCohort() {

    return new CreateCohortOper(createReqSpec());

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The request execution methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public &amp;lt;T&amp;gt; T execute(Function&amp;lt;Response, T&amp;gt; handler) {

    return handler.apply(

            RestAssured.given()

                    .spec(reqSpec.build())

                    .expect()

                    .spec(respSpec.build())

                    .when()

                    .request(REQ_METHOD, REQ_URI)

    );

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And typed response deserialization:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public CreateCohortsRs executeAs(Function&amp;lt;Response, Response&amp;gt; handler) {

    TypeRef&amp;lt;CreateCohortsRs&amp;gt; type = new TypeRef&amp;lt;CreateCohortsRs&amp;gt;() {};

    return execute(handler).as(type);

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So execute(...), executeAs(...), .idPath(...), .pageQuery(...), .body(...) aren't RestAssured magic on their own. They're the shape of the generated client, which we get from OpenAPI Generator plus our templates.&lt;/p&gt;

&lt;p&gt;pojo.mustache handles the models — CreateCohortsRq, CreateCohortsRs, ListUsersWithPagination. It describes what the generated model classes look like: fields from the Swagger schema, Jackson annotations, getters and setters, builder-style methods, equals/hashCode/toString.&lt;/p&gt;

&lt;p&gt;In practice this lets us keep the convenience of generation while controlling the shape of what comes out: Allure steps, a fluent API, execute/executeAs, how RequestSpecification is handled, and the style of the models.&lt;br&gt;
Wiring it together with Gradle&lt;br&gt;
OpenAPI Generator is added in build.gradle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;plugins {

    id "org.openapi.generator" version "7.18.0"

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generated code is registered as a test source set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sourceSets {

    test {

        java {

            srcDir('build/generate-resources/src/main/java')

        }

    }

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which means the Java classes OpenAPI Generator drops into build/generate-resources/src/main/java are available in tests like any other test code.&lt;/p&gt;

&lt;p&gt;Each service gets two Gradle tasks: one to download and prepare the Swagger file, one to generate the Java RestAssured client. For Cohorts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tasks.register('downloadCohortsSwaggerFile', Exec) {

    group = "openapi tools"

    def url = "https://example.com/cohort/openapi_cohort.yaml"

    def outputFile = file("${projectDir}/src/test/resources/cohorts.swagger.yaml")

    if (outputFile.exists()) {

        commandLine("curl", "-z", outputFile, "-o", outputFile, url)

    } else {

        commandLine("curl", "-o", outputFile, url)

    }

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A note from experience: sometimes the spec needs a little preparation before generation, to get it into a shape that both OpenAPI Generator and the coverage tool handle predictably. This happens when a service's Swagger has grown organically — inconsistent in places, overly generic descriptions in others, not specific enough for the generator somewhere else. I won't go into our internal cleanup details here. The point is that what you feed the generator should reliably produce a client and models.&lt;/p&gt;

&lt;p&gt;Then generation runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tasks.register("openApiCohorts", GenerateTask) {

    dependsOn 'downloadCohortsSwaggerFile'

    apiPackage.set("cohorts.api")

    modelPackage.set("cohorts.model")

    generatorName = "java"

    library = "rest-assured"

    inputSpec.set("$rootDir/src/test/resources/cohorts.swagger.yaml")

    outputDir.set("$buildDir/generate-resources")

    templateDir.set("$rootDir/src/test/resources/openapi-templates")

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Producing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cohorts.api.CohortsApi

cohorts.ApiClient

cohorts.model.CreateCohortsRq

cohorts.model.CreateCohortsRs

cohorts.model.ListUsersWithPagination

...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And to make all of it happen automatically before the tests compile:&lt;/p&gt;

&lt;p&gt;compileTestJava.dependsOn tasks.openApiBanners, tasks.openApiGifts, tasks.openApiCohorts&lt;/p&gt;

&lt;p&gt;So when CI runs the Gradle test task, this chain fires on its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;downloadCohortsSwaggerFile

        ↓

openApiCohorts

        ↓

compileTestJava

        ↓

test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the generated client in tests&lt;br&gt;
Building cohortsDefaultApi&lt;br&gt;
We don't construct a RestAssured request by hand every time. There's a base method that returns the generated API class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class BaseCohortTests {

   protected CohortsApi cohortsDefaultApi() {

       return ApiClient.api(ApiClient.Config.apiConfig().reqSpecSupplier(

               () -&amp;gt; new RequestSpecBuilder()

                       .addRequestSpecification(CohortsSpec.defaultReqSpec())

       )).cohorts();

   }

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A RequestSpecification is passed into the generated client, built in CohortsSpec.defaultReqSpec():&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public static RequestSpecification defaultReqSpec() {

   return new RequestSpecBuilder()

           .setBaseUri(config.COHORTS_URL())

           .setConfig(Specifications.hiddenTokensConfig())

           .addFilter(new AllureRestAssured())

           .addFilter(new SwaggerCoverageV3RestAssured(

                   new FileSystemOutputWriter(Paths.get("build/swagger-coverage-output/cohorts"))))

           .addHeader("x-token", valuesConfig.cohortsToken())

           .addHeader("x-device", "ekb")

           .log(LogDetail.ALL)

           .build();

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two distinct layers here, and keeping them separate is what makes the whole thing maintainable.&lt;/p&gt;

&lt;p&gt;The generated client owns everything that came from Swagger: endpoint, HTTP method, path parameters, query parameters, request body, response model.&lt;/p&gt;

&lt;p&gt;The RequestSpecification owns the infrastructure of a particular test run: base URL, tokens, technical headers, logging, the Allure filter, the Swagger Coverage filter.&lt;/p&gt;

&lt;p&gt;The Allure filter adds HTTP requests and responses to the Allure report — handy for triaging failures, unrelated to client generation. The Swagger Coverage filter does something different: it records the actual HTTP calls into build/swagger-coverage-output so a coverage report can be built after the run.&lt;br&gt;
The test itself&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Test

@DisplayName("Create an empty cohort")

void createCohort() {

    CreateCohortsRq request = CreateCohortsRq.builder()

            .name("Autotest cohort " + UUID.randomUUID())

            .description("Cohort created by an autotest")

            .build();

    CreateCohortsRs result = cohortsDefaultApi()

            .createCohort()

            .body(request)

            .executeAs(response -&amp;gt; response

                    .then()

                    .statusCode(201)

                    .extract().response());

    cohortId = result.getId();

    step("And I verify the cohort was created", () -&amp;gt; {

        assertNotNull(result, "Response must not be null");

        assertNotNull(cohortId, "Created cohort ID must not be null");

        assertNotEquals(0, cohortId, "Created cohort ID must not be 0");

    });

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Almost every line traces back to Swagger: CreateCohortsRq is the request model, createCohort() is the operationId, body(request) is the request body, CreateCohortsRs is the response schema.&lt;br&gt;
Path and query parameters are generated too&lt;br&gt;
Another example — fetching the list of cards in a cohort. In Swagger:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/api/v1/cohort/{id}/users:

  get:

    operationId: getUsers

    parameters:

      - in: path

        name: id

      - in: query

        name: page

      - in: query

        name: size

      - in: query

        name: search_pattern

    responses:

      200:

        content:

          application/json:

            schema:

              $ref: '#/components/schemas/ListUsersWithPagination'

Which generates a fluent API:

operationId: getUsers          → .getUsers()

path parameter id              → .idPath(...)

query parameter page           → .pageQuery(...)

query parameter size           → .sizeQuery(...)

query parameter search_pattern → .searchPatternQuery(...)

response schema                → ListUsersWithPagination result

In the test:

@Test

@DisplayName("Get the list of cards in a cohort")

void getUsers() {

    step("Given cohort ID: %s".formatted(cohortId));

    ListUsersWithPagination result = cohortsDefaultApi()

            .getUsers()

            .idPath(cohortId)

            .pageQuery(PAGE)

            .sizeQuery(PAGE_SIZE)

            .searchPatternQuery(number)

            .executeAs(response -&amp;gt; response

                    .then()

                    .statusCode(200)

                    .extract().response());

    step("And I verify the response body", () -&amp;gt; {

        assertNotNull(result, "Response must not be null");

        assertNotNull(result.getNumbers(), "Card list must not be null");

        assertTrue(result.getNumbers().contains(number), "Card list must contain the test card");

        assertNotNull(result.getPageNumber(), "pageNumber must not be null");

        assertNotNull(result.getPageSize(), "pageSize must not be null");

        assertNotNull(result.getTotalPages(), "totalPages must not be null");

        assertNotNull(result.getTotalCount(), "totalCount must not be null");

    });

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;QA doesn't need to remember what a query parameter is called in the URL. If it's described in Swagger, the generator makes a method for it.&lt;br&gt;
execute vs executeAs&lt;br&gt;
execute(...) makes a plain RestAssured call and hands control back out. Useful when the response has no body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cohortsDefaultApi()

        .addUsers()

        .idPath(cohortId)

        .body(request)

        .execute(Validatable::then)

        .statusCode(204);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;executeAs(...) is for when Swagger describes a response schema. Under the hood it executes the request and deserializes the response into the generated model.&lt;/p&gt;

&lt;p&gt;For us this isn't just a convenience wrapper — it's an extra contract check. If Swagger says the response should be CreateCohortsRs, the test tries to read the actual JSON as exactly that. When the backend returns something that doesn't match the contract, the test fails on deserialization or on the assertions. That catches:&lt;/p&gt;

&lt;p&gt;Swagger calls the field id, the API returns cohort_id&lt;br&gt;
Swagger types id as integer, the API returns a string&lt;br&gt;
Swagger describes an object, the API returns an array&lt;br&gt;
with strict deserialization, the backend added a field and nobody updated Swagger&lt;/p&gt;

&lt;p&gt;Which is how you gradually train a team to keep the contract current — the contract breaking becomes your problem, immediately, instead of a documentation debt nobody feels.&lt;br&gt;
The next problem: we couldn't see what was actually covered&lt;br&gt;
At this point we had a large pile of API tests using generated clients and models, much closer to the contract than the old hand-rolled ones.&lt;/p&gt;

&lt;p&gt;But a different problem appeared: we had no idea how much of the API those tests really covered. Lots of tests, all green — but which Swagger operations do they actually call? Which endpoints are untouched entirely? Which are only partially covered? Checking that by reading code got old fast.&lt;/p&gt;

&lt;p&gt;So we took a different approach: record the actual HTTP calls during the test run, then compare that log against the OpenAPI spec afterwards. Tests run as usual; after the run you get a report showing what part of the contract was exercised.&lt;br&gt;
Swagger Coverage in GitLab CI&lt;br&gt;
For this we use a modified version of swagger-coverage, originally created by Victor Orlovsky. The core idea fit us well: split the process in two stages. During the test run, a RestAssured filter collects the actual HTTP calls. After the run, a separate CI job compares those calls against the OpenAPI spec and builds a coverage report.&lt;/p&gt;

&lt;p&gt;The original project hasn't been maintained for a while, and we hit bugs and limitations while integrating it. So Andrey Poletaev (@fenixnow) forked it, fixed the problems, and adapted the tool to our needs. The fork lives at github.com/fenixnow/swagger-coverage, and its README covers configuration and running it via Docker.&lt;br&gt;
During tests: collecting the calls&lt;br&gt;
The RestAssured request specification includes the coverage filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.addFilter(new SwaggerCoverageV3RestAssured(

        new FileSystemOutputWriter(Paths.get("build/swagger-coverage-output/cohorts"))))

It hooks into the HTTP execution chain and sees every request that goes through our RequestSpecification:

test calls a generated API method

        ↓

generated client executes a RestAssured request

        ↓

RestAssured passes request/response through filters

        ↓

SwaggerCoverageV3RestAssured records the call

        ↓

data is written to build/swagger-coverage-output/cohorts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth being clear about: the filter does not build an HTML report during the test. It only collects raw data — which endpoint was called, with which HTTP method, and what status came back.&lt;br&gt;
The CI jobs&lt;br&gt;
After the tests, separate coverage jobs run in a Docker image containing the modified swagger-coverage:&lt;/p&gt;

&lt;p&gt;swagger-coverage-report--banners&lt;/p&gt;

&lt;p&gt;swagger-coverage-report--gifts&lt;/p&gt;

&lt;p&gt;swagger-coverage-report--cohorts&lt;/p&gt;

&lt;p&gt;Because the image ships swagger-coverage-commandline, each job stays simple — it only needs the spec, the input directory, and a config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;swagger-coverage-commandline \

  -s src/test/resources/cohorts.swagger.yaml \

  -i build/swagger-coverage-output/cohorts \

  -c swagger-coverage-cohorts-config.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where -s is the OpenAPI spec, -i is the data collected during the tests, and -c is the report rules config.&lt;/p&gt;

&lt;p&gt;The job then copies out two artifacts:&lt;/p&gt;

&lt;p&gt;cohorts-coverage-report.html   → a readable report for humans&lt;/p&gt;

&lt;p&gt;cohorts-coverage-results.json  → structured JSON for an LLM&lt;/p&gt;

&lt;p&gt;The HTML report looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuplevgv71il4x2b1iu4a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuplevgv71il4x2b1iu4a.png" alt="html report" width="799" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Getting the links into the Merge Request&lt;br&gt;
The coverage job stores the HTML and JSON URLs in a dotenv artifact, and a separate publish-coverage-report job posts a comment into the Merge Request with links to both report types for every service.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F84gqh01w2205apetlw1p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F84gqh01w2205apetlw1p.png" alt="A bot comment in the Merge Request with HTML and LLM JSON coverage report links per service" width="799" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why two formats: the HTML is for a person — open it in a browser, look at the summary, methods, groups, coverage variants, and quickly see where the gaps are. The LLM JSON exists because HTML is genuinely bad input for a model: awkward to feed in, hard to parse reliably. So the modified swagger-coverage gained a separate JSON export aimed specifically at LLM consumption.&lt;br&gt;
The whole chain&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitLab CI

        ↓

test job runs the Gradle test task

        ↓

Gradle downloads Swagger

        ↓

OpenAPI Generator generates Java clients/models

        ↓

JUnit/RestAssured tests run through the generated client

        ↓

SwaggerCoverageV3RestAssured writes build/swagger-coverage-output

        ↓

GitLab stores swagger files and coverage output as artifacts

        ↓

coverage job runs swagger-coverage-commandline in Docker

        ↓

HTML and JSON coverage reports are generated

        ↓

publish-coverage-report posts the links into the Merge Request

        ↓

an LLM reads the JSON report and knows which tests are still missing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That connection is what makes it automatic. Nobody downloads Swagger by hand, runs the generator, hunts for coverage output, or assembles a report. The pipeline does it, and the team gets a readable HTML report plus a JSON file suitable for model analysis.&lt;br&gt;
Connecting an LLM to API coverage&lt;br&gt;
Once we had coverage reports, the obvious next step was to use them as structured input for an LLM rather than just a web page for humans.&lt;/p&gt;

&lt;p&gt;The JSON report contains a coverage summary, a list of paths, the state of each operation, and the requirements not yet closed by tests. Simplified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{

  "summary": {

    "total_operations": 89,

    "fully_covered": 5,

    "partially_covered": 20,

    "not_covered": 128,

    "coverage_percent": 13.0

  },

  "paths": {

    "/api/v1/cohort/{id}/users": {

      "GET": {

        "state": "PARTY",

        "coverage": "3/6",

        "deprecated": false,

        "requirements": {

          "status_codes": [],

          "parameters": [],

          "body": [],

          "properties": []

        }

      }

    }

  }

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some people on the team work in Cursor, others in Claude Code, so we described the test generation rules as skills for both tools.&lt;/p&gt;

&lt;p&gt;The important design decision was splitting the skills by responsibility. The first analyses the coverage report and acts as an orchestrator. The second writes the actual tests.&lt;br&gt;
First skill: analysis and orchestration&lt;br&gt;
It takes two files as input — cohorts.swagger.yaml and cohorts-coverage-results.json. The spec tells it the contract: endpoints, operationIds, parameters, request bodies, response models, statuses. The coverage JSON tells it what's covered and what isn't.&lt;/p&gt;

&lt;p&gt;It reads the operation states:&lt;/p&gt;

&lt;p&gt;EMPTY → endpoint not covered by tests at all&lt;/p&gt;

&lt;p&gt;PARTY → endpoint partially covered&lt;/p&gt;

&lt;p&gt;FULL  → endpoint fully covered&lt;/p&gt;

&lt;p&gt;Then looks at the requirements to work out what specifically is missing:&lt;/p&gt;

&lt;p&gt;status_codes → a required HTTP status was never asserted&lt;/p&gt;

&lt;p&gt;parameters   → a path/query/header parameter was never used&lt;/p&gt;

&lt;p&gt;body         → the request body isn't covered&lt;/p&gt;

&lt;p&gt;properties   → response fields were never checked&lt;/p&gt;

&lt;p&gt;And decides what to do:&lt;/p&gt;

&lt;p&gt;EMPTY endpoint → propose a basic happy-path test&lt;br&gt;
PARTY endpoint → add the missing assertions or scenarios&lt;br&gt;
FULL endpoint → skip&lt;br&gt;
deprecated endpoint → skip&lt;/p&gt;

&lt;p&gt;Then it calls the second skill and passes not the whole report, but a specific task: which endpoint to cover, which operationId to use, which request/response schemas to look at, which requirements to close.&lt;br&gt;
Second skill: generating tests to the project's rules&lt;br&gt;
The second skill doesn't analyse anything globally. It receives a concrete task, reads the OpenAPI spec, finds the right generated API method and models, checks the existing tests, and only then writes the new test.&lt;/p&gt;

&lt;p&gt;It follows our constraints:&lt;/p&gt;

&lt;p&gt;use the generated Swagger client&lt;br&gt;
use the generated models&lt;br&gt;
don't hand-write raw JSON&lt;br&gt;
don't invent your own DTOs&lt;br&gt;
don't assemble URLs manually&lt;br&gt;
put tests in the correct package&lt;br&gt;
pick executeAs or execute according to the contract&lt;br&gt;
assert through the generated models' getters&lt;br&gt;
wrap steps in Allure&lt;/p&gt;

&lt;p&gt;So for an uncovered Cohorts endpoint, it goes to Swagger, finds the operation, reads the operationId and schemas, and works out which generated code to use:&lt;/p&gt;

&lt;p&gt;API method              → cohortsDefaultApi().getUsers()&lt;/p&gt;

&lt;p&gt;path parameter id       → .idPath(...)&lt;/p&gt;

&lt;p&gt;query parameter page    → .pageQuery(...)&lt;/p&gt;

&lt;p&gt;query parameter size    → .sizeQuery(...)&lt;/p&gt;

&lt;p&gt;response model          → ListUsersWithPagination&lt;/p&gt;

&lt;p&gt;execution method        → executeAs(...)&lt;/p&gt;

&lt;p&gt;This is the crux of it: the LLM must not invent its own way of talking to the API. It has to use the same contract-bound client that every other test uses. Otherwise you get tests that pass while quietly bypassing the very mechanism that makes the suite trustworthy.&lt;br&gt;
When Swagger alone isn't enough&lt;br&gt;
Being honest about the limits: not every test can be generated from Swagger and a coverage report.&lt;/p&gt;

&lt;p&gt;For simple CRUD it's often plenty — there's an endpoint, a clear request body, a clear response body, and you can build a happy path with basic assertions.&lt;/p&gt;

&lt;p&gt;But complex microservices exist where calling a single method requires a lot of preconditions:&lt;/p&gt;

&lt;p&gt;create an entity in another system&lt;br&gt;
prepare a user in a particular state&lt;br&gt;
obtain a valid token&lt;br&gt;
enable a feature flag beforehand&lt;br&gt;
pass parameters you cannot guess from Swagger&lt;br&gt;
respect a business-level sequence of calls&lt;/p&gt;

&lt;p&gt;Swagger describes the contract of one HTTP method well. It doesn't explain business context: where the data comes from, what state the system must be in, which steps precede the call.&lt;/p&gt;

&lt;p&gt;For those cases we need test cases written by QA, with preconditions, steps, test data, and expected results. So we have separate skills that work from test cases in Allure TestOps instead of from a coverage report. We built our own MCP server for TestOps so an LLM can pull a test case, parse it, and turn it into an autotest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Allure TestOps test case

        ↓

our MCP server for Allure TestOps

        ↓

an LLM skill reads preconditions, steps, and expected result

        ↓

the skill maps the steps onto the OpenAPI spec

        ↓

finds the generated API methods and models

        ↓

writes the test to the project's rules
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So we're not trying to replace QA and test design with Swagger alone. Swagger owns the contract, coverage owns visibility into the gaps, and test cases own the complex business scenarios where a human description of preconditions and expected behaviour is unavoidable.&lt;br&gt;
What about gRPC&lt;br&gt;
Every example here is deliberately REST, where OpenAPI is the contract.&lt;/p&gt;

&lt;p&gt;For gRPC the idea is the same — tests should lean on the contract — but instead of Swagger you have .proto files generating service stubs, RPC methods, and request/response messages. The technical implementation differs enough (different transport, different contract format, different generated classes, a different approach to coverage) that mixing both into one article would help nobody. REST here; gRPC deserves its own write-up.&lt;br&gt;
Keeping generated code and test code apart&lt;br&gt;
There are two kinds of code in the project.&lt;/p&gt;

&lt;p&gt;Generated from Swagger, never written by hand:&lt;/p&gt;

&lt;p&gt;cohorts.api.CohortsApi&lt;/p&gt;

&lt;p&gt;cohorts.ApiClient&lt;/p&gt;

&lt;p&gt;cohorts.model.CreateCohortsRq&lt;/p&gt;

&lt;p&gt;cohorts.model.CreateCohortsRs&lt;/p&gt;

&lt;p&gt;cohorts.model.ListUsers&lt;/p&gt;

&lt;p&gt;cohorts.model.ListUsersWithPagination&lt;/p&gt;

&lt;p&gt;And our test code — BaseCohortTests, CohortsSpec, CohortV1Tests — responsible for scenarios, steps, assertions, tokens, base URLs, and wiring up filters.&lt;/p&gt;

&lt;p&gt;Simplified, the division of responsibility runs like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Swagger describes the contract

        ↓

OpenAPI Generator creates API classes and models

        ↓

Mustache templates define the shape of the generated code

        ↓

BaseCohortTests connects the generated client to the test infrastructure

        ↓

CohortV1Tests describes a specific test scenario
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generated code owns the technical API contract; our test code owns scenarios, data, steps, and assertions.&lt;br&gt;
What this buys us&lt;br&gt;
The main benefit is that the tests sit close to the contract.&lt;/p&gt;

&lt;p&gt;Change an operationId in Swagger and the generated method name changes. Change the request body and the generated model changes. Remove a field from a response and the getter disappears. A lot of divergence becomes visible at compile time or during deserialization — not a week after release.&lt;/p&gt;

&lt;p&gt;And the test stays readable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CreateCohortsRs result = cohortsDefaultApi()

        .createCohort()

        .body(request)

        .executeAs(response -&amp;gt; response

                .then()

                .statusCode(201)

                .extract().response());
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From that chain you can immediately see which API is called, what body is sent, what status is expected, and which model the response is deserialized into.&lt;/p&gt;

&lt;p&gt;Swagger Coverage closes the second part of the problem: which operations from the spec were actually exercised, and which are still untouched. The LLM skills close the third: turning that report into new tests — not randomly, but to the project's rules. And test cases from TestOps cover the complex scenarios where Swagger isn't enough on its own.&lt;/p&gt;

&lt;p&gt;The end result is that Swagger stops being a documentation page. It becomes a working contract between analysts, developers, and QA — and the coverage report stops being a pretty HTML artifact and becomes a backlog for the test suite.&lt;br&gt;
What's next&lt;br&gt;
This covered one slice of a bigger process: contract-driven API tests and a coverage report an LLM can read.&lt;/p&gt;

&lt;p&gt;Next in the series: how we triage failed autotests through defects in Allure TestOps, tie that to an issue tracker, and use AI not only to generate tests but to analyse results and file tasks. And after that, a system where AI connects to our services, prepares test plans itself, verifies a task through the database, REST, and gRPC, and posts the final report straight into the tracker issue.&lt;/p&gt;

&lt;p&gt;If you've built something similar on top of OpenAPI, I'd be curious what your coverage numbers looked like the first time you measured them. Ours were humbling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let’s Work Together
&lt;/h2&gt;

&lt;p&gt;I’m open to QA leadership opportunities and collaborations with teams looking to improve their testing strategy, build scalable test automation, introduce AI-assisted QA workflows, and establish meaningful quality metrics.&lt;/p&gt;

&lt;p&gt;If this sounds relevant to your team, feel free &lt;a href="https://www.linkedin.com/in/oleg-malyshev/" rel="noopener noreferrer"&gt;to connect with me on LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How We Migrated Test Cases from a Task Tracker to Allure TestOps with a Single Command Using Cursor + MCP</title>
      <dc:creator>Oleg Malyshev</dc:creator>
      <pubDate>Thu, 06 Aug 2026 20:24:34 +0000</pubDate>
      <link>https://dev.to/tanki_sleva/how-we-migrated-test-cases-from-a-task-tracker-to-allure-testops-with-a-single-command-using-cursor-1bpp</link>
      <guid>https://dev.to/tanki_sleva/how-we-migrated-test-cases-from-a-task-tracker-to-allure-testops-with-a-single-command-using-cursor-1bpp</guid>
      <description>&lt;p&gt;Hi! I’m Oleg, a QA Engineering Manager focused on test strategy, automation, and AI-assisted QA workflows.&lt;/p&gt;

&lt;p&gt;I lead one of the testing stacks at a large grocery retail company. We've been working through a series of experiments on where AI actually saves time in QA — not where it looks impressive in a demo, but where it removes hours of mechanical work that nobody wants to do.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/tanki_sleva/how-we-built-an-ai-test-case-reviewer-using-cursor-mcp-and-allure-testops-1e9j"&gt;An earlier piece covered AI review of manual test cases.&lt;/a&gt; This one is about a less typical use of Cursor: migrating test cases from a task tracker into Allure TestOps with essentially one command.&lt;/p&gt;

&lt;p&gt;Two bits of context for readers outside our stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Yandex Tracker&lt;/strong&gt; is an issue tracker, roughly analogous to Jira. Its issue descriptions support wiki-style tables, which matters here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allure TestOps&lt;/strong&gt; is a test management system (TMS) that connects manual test cases with automated tests, CI/CD runs, and reporting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you use Jira and TestRail, or Linear and Xray, the specific tools change but the pattern in this article doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: test cases living in the wrong place
&lt;/h2&gt;

&lt;p&gt;Historically, one large team kept all of its test cases and checklists inside the issue tracker. Each issue had a "Testing" section containing a table, and that table was the team's actual test documentation.&lt;/p&gt;

&lt;p&gt;Then the inevitable happened — we needed all of it in Allure TestOps, because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it's the correct place for it (a single TMS instead of test cases scattered across issues);&lt;/li&gt;
&lt;li&gt;it gives us analytics, traceability, and artifacts;&lt;/li&gt;
&lt;li&gt;it links properly to automated tests and CI/CD;&lt;/li&gt;
&lt;li&gt;and the rest of the company already lives in TestOps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The catch was volume. Years of accumulated cases and checklists. Migrating them by hand meant a large amount of pure copy-paste work for QA engineers, which is exactly the kind of task you don't want to hand to people if you can avoid it.&lt;/p&gt;

&lt;p&gt;So the plan was to migrate everything quickly, with no manual copy-paste, using an AI agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The approach: Cursor + two MCP servers + two prompts
&lt;/h2&gt;

&lt;p&gt;The plan looked suspiciously simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take Cursor.&lt;/li&gt;
&lt;li&gt;Connect the Yandex Tracker MCP server.&lt;/li&gt;
&lt;li&gt;Connect the Allure TestOps MCP server (ours, built in-house).&lt;/li&gt;
&lt;li&gt;Write a project rule that:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;  takes the "Testing" table from the issue description,&lt;/li&gt;
&lt;li&gt;  converts it into a "title + steps" structure,&lt;/li&gt;
&lt;li&gt;  collects links to the source issue and its related issues.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run it as a single command.&lt;/p&gt;

&lt;p&gt;For anyone new to it: MCP (Model Context Protocol) is a standard way to expose external systems as tools an AI agent can call. Instead of the agent guessing, it gets real API access to the tracker and to TestOps.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the source data looked like
&lt;/h2&gt;

&lt;p&gt;Inside each tracker issue, the test cases lived in a table under a "Testing" section, structured roughly like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an "Acceptance criterion" column,&lt;/li&gt;
&lt;li&gt;a "Test" column,&lt;/li&gt;
&lt;li&gt;other columns (outcome, result, comments, and so on) that exist but are irrelevant to the migration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We agreed on a simple mapping:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Acceptance criterion → test case title in TestOps&lt;/li&gt;
&lt;li&gt;Test → scenario steps in TestOps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else gets dropped.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdzcx6lnmbevw6n8w1xgx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdzcx6lnmbevw6n8w1xgx.png" alt="What we migrated from: a " width="800" height="1127"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One wrinkle worth flagging early, because it's the kind of thing that quietly corrupts a migration: the tables contain visually merged cells. When several test rows belong to one acceptance criterion, only the first row has the criterion filled in and the rest are blank. A naive parser produces a pile of test cases with empty titles.&lt;/p&gt;

&lt;h2&gt;
  
  
  The project rule: parsing the tracker and producing JSON
&lt;/h2&gt;

&lt;p&gt;The first artifact is a project rule describing exactly how to parse the table and what JSON to return. I called the file get-check-list-rule. Here it is, translated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a QA Automation / test analyst assistant. Your task: given a tracker issue key {ISSUE_KEY}, extract the "Testing" section (a table) from its description and prepare structured data for migration into Allure TestOps.

Input {ISSUE_KEY} — the issue key (for example, LKP-3260).

What to do

Fetch the issue via the issue_get tool with include_description=true. Fetch related issues via issue_get_links.

In the description field, find the "Testing" section block. It starts with a line reading "Testing" and contains a table where the table opens with #|, followed by rows starting with || and the column headers, and closes with |#.

Parse the table and extract all test rows:

Relevant columns: "Acceptance criterion", "Test".
All other columns and their values (for example "Acceptance outcome", "Result", "Comment") must be completely ignored and carried nowhere — not into titles, not into steps, not into links.

Account for merged cells:

If the "Acceptance criterion" column is empty in a row, inherit the last non-empty value above it.
The same technique applies to other columns if needed, though in practice only the criterion requires it.

Group results by acceptance criterion. For each criterion, build:

title: the criterion text, which becomes the test case title in TestOps, without leading numbering. If the criterion starts with 1., 1), 1\. or similar, strip it. Normalize the same way as steps: plain text, no markdown or HTML, no redundant whitespace or blank lines. If the criterion spans several lines, join them into one with spaces.
steps: the list of steps from the "Test" column for every row belonging to that criterion.

Normalize steps:

Remove redundant whitespace and repeated blank lines.
Steps must arrive in Allure TestOps as plain text — no HTML tags, no markdown.

Converting "Test" into plain text:

Strip or unwrap markdown formatting: bold, italics, # headings, &amp;gt; quotes, inline code and backticks, fenced code blocks — keep the text only.
Convert markdown links [text](url) into text (url), or just url when there is no text.
Convert lists (-, *, numbered) into multi-line plain text, preserving order but dropping the markers and numbers.

Important: if the "Test" column contains fragments that actually belong to other columns (PASS/FAIL/BLOCK markers, checkboxes, comments, references like screen/log from the "Comment" column) — do not migrate them. Migrate only what genuinely describes scenario steps.

Strip explicit numbering from the start of step lines where present (1. ..., 2) ..., a) ...) — steps[] must contain unnumbered steps.

Important: do not add a line like "Source: {ISSUE_KEY}" into the steps or description. Issue links belong in the Links (external links) section in Allure TestOps, not inline in the description.

Important, on related issues:

In the TestOps Links section, add a link to the source issue {ISSUE_KEY}, plus links to all issues related to it from issue_get_links.
Order matters: {ISSUE_KEY} always comes first (the source issue the tests came from), followed by the related issues (their object.key), with no duplicates and without repeating {ISSUE_KEY}.

Link naming rule, so the source is always first in the Links list:

For the source issue {ISSUE_KEY}, use the link name Source: {ISSUE_KEY}.
For related issues, use a link name equal to their key (for example LKP-4106) with no prefix.

Return the result strictly as JSON in this shape, with no surrounding text:

json
{
  "issueKey": "{ISSUE_KEY}",
  "linkIssueToTestOps": {
    "field": "Links",
    "keys": ["{ISSUE_KEY}", "&amp;lt;LINKED_ISSUE_KEY_1&amp;gt;", "&amp;lt;LINKED_ISSUE_KEY_2&amp;gt;"]
  },
  "testops": [
    {
      "name": "&amp;lt;Acceptance criterion&amp;gt;",
      "steps": [
        "&amp;lt;Step 1 from the Test column, unnumbered, plain text&amp;gt;",
        "&amp;lt;Step 2 from the Test column, unnumbered, plain text&amp;gt;"
      ]
    }
  ]
}

Quality checks

testops[] must contain every criterion from the "Testing" table, not just the first one.
If there is no "Testing" table, return JSON with an empty testops array and an error field explaining why.
If there are several "Testing" tables (for example, one per environment), extract each and add an environment field (stage, test, and so on) when it can be determined from the surrounding text.

A note on the literal strings: in our tracker the section and column headings are in Russian, so the rule matches on those exact names. If you adapt this, swap in whatever your own tables actually say.

Two details in that rule carry most of the weight. The first is the merged-cell inheritance, which is the difference between a clean migration and a pile of untitled cases. The second is the instruction to return an error field rather than improvise — without it, an agent that can't find a table will happily invent a plausible one, and you won't notice until a QA engineer opens TestOps.

The command: /move, one issue or a batch

The second artifact is the command, which I called move. It:

accepts one or more ISSUE_KEY values (links to the issues to migrate from),
pulls each issue from the tracker,
generates JSON according to get-check-list-rule,
creates the test cases in TestOps and populates the Links section.

The command prompt, translated:

Migrate test cases from the tracker into Allure TestOps, strictly following the project rule @cursor/rules/get-check-list-rule.mdc.

Input

I will give you one of:

a link to a tracker issue (or several links)
issue key(s) (for example LKP-3260, ABC-123)
plus attached screenshots, if wording or context needs clarifying

What to do

Support migrating several issues at once. Example invocation: /move LKP-3260 LKP-3261 LKP-3262
Extract the list of ISSUE_KEY values from the text (support both bare keys and URLs), remove duplicates, preserve order of appearance.
For each ISSUE_KEY:
fetch the issue through the Yandex Tracker MCP (issue_get, include_description=true)
generate JSON strictly according to @.cursor/rules/get-check-list-rule.mdc (including issue_get_links, title/step normalization, and building linkIssueToTestOps.keys)
push the result into Allure TestOps through the user-allure MCP: create test cases from testops[], then add the issue links from linkIssueToTestOps into the Links section, using a link_name of the form Source: &amp;lt;ISSUE_KEY&amp;gt; for the source issue
If the Allure MCP is unavailable or errors out for some issue — do not invent anything. Add an error field to that issue's JSON and carry on processing the rest.

Output (strict)

If one issue was passed — a single JSON object per get-check-list-rule.mdc, with no surrounding text.
If several issues were passed — a JSON array where each element follows the same format.
If there were problems with the Allure MCP for an element, add an error field to that element with a short description. Otherwise, omit it.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A note on the literal strings: in our tracker the section and column headings are in Russian, so the rule matches on those exact names. If you adapt this, swap in whatever your own tables actually say.&lt;/p&gt;

&lt;p&gt;Two details in that rule carry most of the weight. The first is the merged-cell inheritance, which is the difference between a clean migration and a pile of untitled cases. The second is the instruction to return an error field rather than improvise — without it, an agent that can't find a table will happily invent a plausible one, and you won't notice until a QA engineer opens TestOps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The command: /move, one issue or a batch
&lt;/h2&gt;

&lt;p&gt;The second artifact is the command, which I called move. It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;accepts one or more ISSUE_KEY values (links to the issues to migrate from),&lt;/li&gt;
&lt;li&gt;pulls each issue from the tracker,&lt;/li&gt;
&lt;li&gt;generates JSON according to get-check-list-rule,&lt;/li&gt;
&lt;li&gt;creates the test cases in TestOps and populates the Links section.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The command prompt, translated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Migrate test cases from the tracker into Allure TestOps, strictly following the project rule @cursor/rules/get-check-list-rule.mdc.

Input

I will give you one of:

a link to a tracker issue (or several links)
issue key(s) (for example LKP-3260, ABC-123)
plus attached screenshots, if wording or context needs clarifying

What to do

Support migrating several issues at once. Example invocation: /move LKP-3260 LKP-3261 LKP-3262
Extract the list of ISSUE_KEY values from the text (support both bare keys and URLs), remove duplicates, preserve order of appearance.
For each ISSUE_KEY:
fetch the issue through the Yandex Tracker MCP (issue_get, include_description=true)
generate JSON strictly according to @.cursor/rules/get-check-list-rule.mdc (including issue_get_links, title/step normalization, and building linkIssueToTestOps.keys)
push the result into Allure TestOps through the user-allure MCP: create test cases from testops[], then add the issue links from linkIssueToTestOps into the Links section, using a link_name of the form Source: &amp;lt;ISSUE_KEY&amp;gt; for the source issue
If the Allure MCP is unavailable or errors out for some issue — do not invent anything. Add an error field to that issue's JSON and carry on processing the rest.

Output (strict)

If one issue was passed — a single JSON object per get-check-list-rule.mdc, with no surrounding text.
If several issues were passed — a JSON array where each element follows the same format.
If there were problems with the Allure MCP for an element, add an error field to that element with a short description. Otherwise, omit it.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point 4 is the one I'd copy into any similar workflow. Partial failure is the normal case in batch migrations, and an agent that stops on the first error is nearly as unhelpful as one that hallucinates through it.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP configuration
&lt;/h2&gt;

&lt;p&gt;For any of this to run, both MCP servers need to be connected: Allure TestOps (ours) and Yandex Tracker.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7w6ik7snfoeebi10g9dt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7w6ik7snfoeebi10g9dt.png" alt="Our in-house Allure TestOps MCP server, connected and enabled in Cursor." width="516" height="121"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp9yd7f77h9y745bykl28.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp9yd7f77h9y745bykl28.png" alt="The Yandex Tracker MCP server. Both need to be enabled before the command will run." width="471" height="181"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Example config for the tracker MCP:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;"yandex-tracker": {&lt;br&gt;
  "command": "uvx",&lt;br&gt;
  "args": [&lt;br&gt;
    "yandex-tracker-mcp@latest"&lt;br&gt;
  ],&lt;br&gt;
  "env": {&lt;br&gt;
    "TRACKER_TOKEN": "your token",&lt;br&gt;
    "TRACKER_ORG_ID": "your org id"&lt;br&gt;
  }&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like for a QA engineer
&lt;/h2&gt;

&lt;p&gt;Exactly one thing: run the command with the issue keys.&lt;/p&gt;

&lt;p&gt;/move LKP-3260&lt;/p&gt;

&lt;p&gt;or, as a batch:&lt;/p&gt;

&lt;p&gt;/move LKP-3260 LKP-3261 LKP-3262&lt;/p&gt;

&lt;p&gt;LKP-* are the issues to migrate test cases from, and a project number identifies the TestOps project to migrate them into.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fozrsttk379wewhqtvlc3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fozrsttk379wewhqtvlc3.png" alt="Migrate tests" width="410" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's the whole interface. No spreadsheet, no intermediate export, no clipboard.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The result&lt;/li&gt;
&lt;li&gt;After a run, the migration lands as expected:
test cases appear in TestOps, with titles and steps;
in the Links section, the source issue comes first, labeled Source: , followed by all related issues.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftr6c5txdu6obn4bse6k8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftr6c5txdu6obn4bse6k8.png" alt="testops result" width="800" height="698"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That last part turned out to matter more than we expected. Test cases that carry a link back to the issue they came from stay traceable — six months later you can still see why a case exists, which is usually the first thing lost in a bulk migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where else this pattern fits
&lt;/h2&gt;

&lt;p&gt;The specific tools here are incidental. The shape of the problem — structured content trapped in the wrong system, in a format that's regular enough to parse but irregular enough to defeat a simple script — is extremely common.&lt;/p&gt;

&lt;p&gt;The same setup would move requirements documentation out of Jira issues into Confluence with a Jira MCP and a Confluence MCP, or the reverse. Anywhere you have two systems with API access and a mechanical transformation between them, two prompts and an agent will beat a weekend of copy-paste.&lt;/p&gt;

&lt;p&gt;We also could have written a prompt that iterated across every issue in the tracker and migrated everything automatically, saving QA even more time. We decided we didn't need all of it migrated yet — which is its own small lesson. The value came from removing a specific painful backlog, not from automating the maximum possible surface area.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let’s Work Together
&lt;/h2&gt;

&lt;p&gt;I’m open to QA leadership opportunities and collaborations with teams looking to improve their testing strategy, build scalable test automation, introduce AI-assisted QA workflows, and establish meaningful quality metrics.&lt;/p&gt;

&lt;p&gt;If this sounds relevant to your team, feel free &lt;a href="https://www.linkedin.com/in/oleg-malyshev/" rel="noopener noreferrer"&gt;to connect with me on LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How We Built an AI Test Case Reviewer Using Cursor, MCP, and Allure TestOps</title>
      <dc:creator>Oleg Malyshev</dc:creator>
      <pubDate>Mon, 27 Jul 2026 20:56:57 +0000</pubDate>
      <link>https://dev.to/tanki_sleva/how-we-built-an-ai-test-case-reviewer-using-cursor-mcp-and-allure-testops-1e9j</link>
      <guid>https://dev.to/tanki_sleva/how-we-built-an-ai-test-case-reviewer-using-cursor-mcp-and-allure-testops-1e9j</guid>
      <description>&lt;p&gt;Hi! I’m Oleg, a QA Engineering Manager focused on test strategy, automation, and AI-assisted QA workflows.&lt;/p&gt;

&lt;p&gt;My goal is simple: eliminate repetitive work for QA engineers using AI tools.&lt;/p&gt;

&lt;p&gt;In an ideal world, we provide an AI model with requirements, Figma designs, Git branches, and other project artifacts through MCP. The model then helps us:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create test cases using the available project context.&lt;/li&gt;
&lt;li&gt;Generate automated tests based on those test cases.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  We Decided to Start With AI Test Case Reviews
&lt;/h2&gt;

&lt;p&gt;Writing high-quality test cases from requirements and supporting documentation is not an easy task.&lt;/p&gt;

&lt;p&gt;Instead of trying to automate the entire process immediately, we decided to start with something smaller: an AI-powered test case reviewer.&lt;/p&gt;

&lt;p&gt;The basic idea was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A QA engineer moves a test case to the &lt;strong&gt;Review&lt;/strong&gt; status in Allure TestOps.&lt;/li&gt;
&lt;li&gt;An AI agent retrieves and reviews the test case according to our internal guidelines.&lt;/li&gt;
&lt;li&gt;The agent publishes the review as a new comment on the test case.&lt;/li&gt;
&lt;li&gt;The test case is moved to an appropriate status, such as &lt;strong&gt;Needs Fixing&lt;/strong&gt;, so the QA engineer immediately knows what to do next.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To implement this workflow, we needed the AI agent to be able to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connect to Allure TestOps.&lt;/li&gt;
&lt;li&gt;Retrieve a test case and its scenario.&lt;/li&gt;
&lt;li&gt;Review the content.&lt;/li&gt;
&lt;li&gt;Publish the review back to TestOps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this purpose, we created our own MCP server for Allure TestOps, along with an interface for configuring review prompts for individual TestOps projects.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F41rze68vghpz0iqztftm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F41rze68vghpz0iqztftm.png" alt="Web interface for configuring AI test case review prompts" width="800" height="225"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why We Did Not Use the Built-in Allure TestOps MCP
&lt;/h2&gt;

&lt;p&gt;Allure TestOps recently introduced its own MCP integration and an AI chat interface directly inside the product.&lt;/p&gt;

&lt;p&gt;However, the AI chat requires a separate license. We also discovered the official MCP integration only after we had already developed our own solution.&lt;/p&gt;

&lt;p&gt;After examining the built-in TestOps AI functionality, we concluded that our implementation covered our current needs at least as well.&lt;/p&gt;

&lt;p&gt;Our solution also remains free for us within our existing infrastructure and software licenses.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Our MCP Server Works
&lt;/h2&gt;

&lt;p&gt;Our MCP server is written in Python using the &lt;a href="https://moge.ai/ru/product/fastmcp2" rel="noopener noreferrer"&gt;FastMCP framework&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We also converted the OpenAPI specification provided by Allure TestOps into a generated API client.&lt;/p&gt;

&lt;p&gt;The resulting workflow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The MCP server calls the TestOps API.&lt;/li&gt;
&lt;li&gt;It retrieves the required test case data, including its scenario, steps, attachments, and other fields.&lt;/li&gt;
&lt;li&gt;The data is passed to an LLM for analysis.&lt;/li&gt;
&lt;li&gt;After processing, the result is saved back to TestOps through MCP—for example, as a new review comment.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Allure TestOps
      ↓
MCP server
      ↓
Language model
      ↓
MCP server
      ↓
TestOps comment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why We Used Cursor
&lt;/h2&gt;

&lt;p&gt;When we started working on the prototype, we had several practical limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The web interface for reviewing test cases was not completely ready.&lt;/li&gt;
&lt;li&gt;Our MCP server could occasionally fail to return some parts of a TestOps scenario.&lt;/li&gt;
&lt;li&gt;The language model used by the main solution required separate payment.&lt;/li&gt;
&lt;li&gt;Our team already had access to corporate Cursor accounts and experience using Cursor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I therefore created a minimal project that could be opened in Cursor.&lt;/p&gt;

&lt;p&gt;The goal was to let any QA engineer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clone the repository.&lt;/li&gt;
&lt;li&gt;Open it in Cursor.&lt;/li&gt;
&lt;li&gt;Run a single command to review a test case.&lt;/li&gt;
&lt;li&gt;Quickly evaluate the quality of the AI-generated review.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How the Cursor Project Is Structured
&lt;/h2&gt;

&lt;p&gt;The project contains only a few components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One &lt;strong&gt;project rule&lt;/strong&gt; containing the main test case review prompt.&lt;/li&gt;
&lt;li&gt;One command: &lt;code&gt;/review-test-case&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;One &lt;strong&gt;user rule&lt;/strong&gt; that ensures Cursor loads the project rules.&lt;/li&gt;
&lt;li&gt;An MCP server for interacting with Allure TestOps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fldvtnu0a0zr0vgdk20c4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fldvtnu0a0zr0vgdk20c4.png" alt="Cursor project structure with MCP configuration and review rules" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  User Rule
&lt;/h2&gt;

&lt;p&gt;We added a user rule because Cursor did not always load the project rules consistently.&lt;/p&gt;

&lt;p&gt;The rule explicitly instructs Cursor to read the project rules before every response. A fixed phrase at the beginning of the response serves as a visible indication that the rules were actually applied.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Respond in Russian.

Before every response, read the project rules from .cursor/rules.
Do not begin answering until you have read the rules.

Start every response, without exception, with the phrase:
"Rules added to context: X, Y, Z, etc."

List the names of the rule files you have read.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a workaround rather than an ideal architecture, but it made the behavior easier to verify during our initial experiments.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Test Case Review Prompt
&lt;/h2&gt;

&lt;p&gt;The following prompt is still experimental. We created it to obtain initial results quickly and determine which areas required further improvement.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a Senior QA Engineer with more than 30 years of
experience testing mobile, web, and API applications.

You review test cases in Allure TestOps and help QA engineers
write cases that are:

- unambiguous;
- verifiable;
- atomic;
- reproducible;
- deterministic;
- suitable for automation.

I will provide screenshots or text from an Allure TestOps test
case. Sometimes only some of its fields will be available.

Your task is to review the test case quickly and strictly
according to the rules below and propose specific corrections.

=== REVIEW GUIDELINES ===

1. General principles

Check the test case for:

- unambiguity;
- verifiability, including measurable expected results;
- atomicity;
- independence and repeatability;
- determinism, including fixed feature flags and test data;
- suitability for automation.

Remove vague phrases such as "verify that it works correctly"
or "pay attention to."

2. Terminology

Use the same terms that appear in the product UI.

If UI terminology differs from API or domain terminology,
explain the difference.

3. Required fields

A test case must include:

- Title;
- Preconditions containing only system state and test data;
- Shared steps if the same sequence appears in two or more cases;
- A scenario with an Expected Result for every step;
- Relevant artifacts;
- Tags and links.

4. Title

4.1 Mobile and frontend tests:

&amp;lt;Screen or section&amp;gt; — &amp;lt;What is being tested&amp;gt; —
&amp;lt;Expected effect&amp;gt;

4.2 API tests in a separate API project:

Suite = &amp;lt;endpoint or context&amp;gt;

Title = &amp;lt;scenario or variation&amp;gt;

Do not include generic terms such as Positive, Negative,
Search, or HTTP in the test case title.

5. Preconditions

Do not include instructions such as:

- Read the documentation.
- Prepare Postman.
- Import the cURL request.

These are actions, not system states.

Specify test data using either exact values or criteria for
finding suitable data. Also explain where to obtain it.

6. Steps

One step must contain one action.

Every Expected Result must be measurable. Specify exactly what
should be displayed, which status code should be returned,
which fields should be present, or which state should change.

=== REQUIRED RESPONSE FORMAT ===

A) Short assessment

✅ What is good

Provide 3–6 points.

❌ Problems and risks

Provide 3–8 points.

Be specific. Examples:

- Expected Result is not measurable.
- Preconditions contain instructions instead of state.
- Test data or selection criteria are missing.
- Several checks are mixed into one test case.
- The scenario is not deterministic.
- Terminology differs from the product.
- Repeated actions should be moved to a shared step.

B) Corrections: before → after

Use the following structure for every correction:

1. What is wrong:
   One sentence explaining why it must be changed.

2. Before:
   A short quotation or summary of the current text.

3. After:
   A concise improved version.

Provide corrections for the following areas:

- Title;
- Preconditions;
- Shared steps, if applicable;
- Scenario, including steps and an Expected Result for every step;
- Artifacts, tags, and links, if applicable.

Important:

- Do not write long theoretical explanations.
- Provide only the review and specific corrections.
- Avoid vague statements such as "the link opens."
- Specify where and how it opens: system browser or WebView,
  expected URL or title, close button, and absence of errors.
- If navigation, token preparation, or environment setup is
  repeated, explicitly recommend moving it to a shared step.
- If an exact value is unknown, use placeholders such as
  {banner_id}, {token}, or {card_number}.
- Explain how to find suitable data and verify that it is current.

=== INPUT DATA ===

The user provides the ID of the test case that must be reviewed.

Start the response immediately with section A, followed by
section B.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The &lt;code&gt;/review-test-case&lt;/code&gt; Command
&lt;/h2&gt;

&lt;p&gt;To allow QA engineers to launch a review with a single command, I added the &lt;code&gt;review-test-case&lt;/code&gt; command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# review-test-case

Review a test case in Allure TestOps and publish the result
as a NEW comment on that test case through the TestOps MCP server.

Important:

Always create a new comment.

Never update an existing comment and never delete comments.

Input parameter:

{testcase} — a test case URL or ID.

Data priority:

1. Retrieve the test case data through MCP using
   testcasegetbyid and testcasegetscenario.

2. If some fields in the API response are empty—for example,
   scenario: null or steps: []—but a screenshot is attached
   to the message, use the information visible in the screenshot.

3. Do not state that the scenario or field is empty if its
   contents are visible in the attached image.

Example:

/review-test-case 70756

Response requirements:

- Start with section A, followed by section B.
- Section A must contain:
  - ✅ What is good: 3–6 points.
  - ❌ Problems and risks: 3–8 points.
- Section B must provide "before → after" corrections for:
  - Title;
  - Preconditions;
  - Shared steps, if applicable;
  - Scenario, including an Expected Result for every step;
  - Artifacts, tags, and links, if applicable.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3tbk2tpej3hjx4hch1ge.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3tbk2tpej3hjx4hch1ge.png" alt="Cursor command that starts an Allure TestOps test case review" width="618" height="259"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why We Added a Second MCP Server for Sequential Thinking
&lt;/h2&gt;

&lt;p&gt;We connected two MCP servers to the project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The TestOps MCP server, which retrieves test cases and scenarios and publishes comments.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;sequential-thinking&lt;/code&gt; MCP server, which encourages the model to analyse the test case step by step.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We expected sequential analysis to be useful for test case reviews in the same way that it can be useful for code-related tasks.&lt;/p&gt;

&lt;p&gt;The model can examine the test case in the following order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Preconditions
    ↓
Steps
    ↓
Expected results
    ↓
Test data
    ↓
Determinism
    ↓
Suitability for automation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This process helps expose common problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vague or immeasurable expected results.&lt;/li&gt;
&lt;li&gt;Missing test data.&lt;/li&gt;
&lt;li&gt;Several independent checks mixed into one test case.&lt;/li&gt;
&lt;li&gt;Environment or feature-flag dependencies that are not documented.&lt;/li&gt;
&lt;li&gt;Steps that cannot be automated reliably.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is currently an observation from our experiments rather than proof that the additional MCP server improves every review.&lt;/p&gt;

&lt;p&gt;We still need to evaluate its effect using a larger and more representative set of test cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Workflow Looks Like in Practice
&lt;/h2&gt;

&lt;p&gt;The process is intentionally simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the required test case in Allure TestOps.&lt;/li&gt;
&lt;li&gt;Run the following command in Cursor:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/review-test-case &amp;lt;test-case-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Attach a screenshot if necessary.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The screenshot acts as a fallback because the MCP server may occasionally return an incomplete scenario.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cursor retrieves the test case through MCP.&lt;/li&gt;
&lt;li&gt;The model reviews the test case using the project guidelines.&lt;/li&gt;
&lt;li&gt;The MCP server publishes the result as a new comment in Allure TestOps.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A few seconds later, the QA engineer can see the review directly in the test case.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fff1onxj1b62gcwuvlh3o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fff1onxj1b62gcwuvlh3o.png" alt="test-case" width="800" height="579"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqgv7hoketyy80x7loy6p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqgv7hoketyy80x7loy6p.png" alt="AI-generated review published as a comment in Allure TestOps" width="800" height="996"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Learned
&lt;/h2&gt;

&lt;p&gt;This prototype is deliberately small.&lt;/p&gt;

&lt;p&gt;It does not attempt to replace an experienced QA engineer or automatically approve test cases. Its purpose is to identify common quality problems and provide specific corrections before a human reviewer examines the case.&lt;/p&gt;

&lt;p&gt;Even this minimal implementation can help detect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ambiguous titles.&lt;/li&gt;
&lt;li&gt;Instructions incorrectly placed in preconditions.&lt;/li&gt;
&lt;li&gt;Missing test data.&lt;/li&gt;
&lt;li&gt;Steps without expected results.&lt;/li&gt;
&lt;li&gt;Non-measurable expected results.&lt;/li&gt;
&lt;li&gt;Repeated actions that should become shared steps.&lt;/li&gt;
&lt;li&gt;Scenarios that depend on undocumented state.&lt;/li&gt;
&lt;li&gt;Test cases that are difficult to automate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main advantage is that the result appears inside the tool where QA engineers already work.&lt;/p&gt;

&lt;p&gt;They do not need to copy the test case into a separate chatbot and then manually transfer the review back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The complete workflow is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;QA engineer
    ↓
Cursor command
    ↓
TestOps MCP
    ↓
Test case data
    ↓
LLM review
    ↓
New TestOps comment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For us, this was a practical way to test AI-assisted test case reviews before building a complete web interface.&lt;/p&gt;

&lt;p&gt;The next steps are to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Improve the review prompt.&lt;/li&gt;
&lt;li&gt;Measure review quality using a larger dataset.&lt;/li&gt;
&lt;li&gt;Reduce incomplete responses from the TestOps API.&lt;/li&gt;
&lt;li&gt;Determine which parts of the workflow can be safely automated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a small prototype, I think the result is promising.&lt;/p&gt;

&lt;p&gt;If there is enough interest, I will publish a follow-up describing how we evaluate review quality and generate automated API tests from TestOps test cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let’s Work Together
&lt;/h2&gt;

&lt;p&gt;I’m open to QA leadership opportunities and collaborations with teams looking to improve their testing strategy, build scalable test automation, introduce AI-assisted QA workflows, and establish meaningful quality metrics.&lt;/p&gt;

&lt;p&gt;If this sounds relevant to your team, feel free to &lt;a href="https://www.linkedin.com/in/oleg-malyshev/" rel="noopener noreferrer"&gt;connect with me on LinkedIn.&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
