<?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>Forem: Stepan Romankov</title>
    <description>The latest articles on Forem by Stepan Romankov (@stepan_romankov).</description>
    <link>https://forem.com/stepan_romankov</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F545133%2F90deb006-3cee-4534-aa8a-36ddf37655aa.jpg</url>
      <title>Forem: Stepan Romankov</title>
      <link>https://forem.com/stepan_romankov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/stepan_romankov"/>
    <language>en</language>
    <item>
      <title>Your AI Should Be Writing Tests. The Unfair Advantage Every Vibe Coder Ignores.</title>
      <dc:creator>Stepan Romankov</dc:creator>
      <pubDate>Mon, 02 Mar 2026 20:28:25 +0000</pubDate>
      <link>https://forem.com/stepan_romankov/your-ai-should-be-writing-tests-the-unfair-advantage-every-vibe-coder-ignores-hnm</link>
      <guid>https://forem.com/stepan_romankov/your-ai-should-be-writing-tests-the-unfair-advantage-every-vibe-coder-ignores-hnm</guid>
      <description>&lt;p&gt;A test is a note you leave for the computer. It says: "this thing works like this, and if it ever stops working like this, let me know."&lt;/p&gt;

&lt;p&gt;That's it. Imagine you built a calculator. You write a note that says "2 + 3 must equal 5." The computer checks this note every time something changes. If your calculator suddenly returns 6, the note fires. You don't need to understand how the calculator works internally. You just know it's broken because 2 + 3 is not 6.&lt;/p&gt;

&lt;p&gt;This is the entire concept.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a test looks like in practice
&lt;/h2&gt;

&lt;p&gt;Before any code, here's the plain-English version:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I have a function called &lt;code&gt;calculatePrice&lt;/code&gt;. I give it an item that costs $10 and a quantity of 3. I expect $30 back. If I get anything else, something is wrong.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In Go, that becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestCalculatePrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;calculatePrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"expected 30, got %d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven lines. The machine runs this, checks the result, and tells you if it's wrong. You can have hundreds of these notes scattered across your project. They run in seconds.&lt;/p&gt;

&lt;p&gt;That's a unit test. "Unit" because it tests one small unit of behavior. Not the whole app. Not the database. Just: does this function do the one thing it's supposed to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters if you vibe code
&lt;/h2&gt;

&lt;p&gt;Here's the scenario. You're building an online store. It has a price calculation function. It works. Users are buying things. Life is good.&lt;/p&gt;

&lt;p&gt;You open ChatGPT or Claude and say: "Add a 10% discount for orders over $100."&lt;/p&gt;

&lt;p&gt;The AI gives you 40 lines of code. It looks right. The discount logic is there. You paste it in, try a $150 order, see a $135 total. Ship it.&lt;/p&gt;

&lt;p&gt;What you didn't notice: the AI rewrote &lt;code&gt;calculatePrice&lt;/code&gt; to handle the discount, and in the process it changed how tax is applied. Orders under $100 now have tax calculated twice. Your $10 item costs $10.80 instead of $10.50. Nobody tells you this because you're not going to manually test every old scenario after every change.&lt;/p&gt;

&lt;p&gt;A test would have caught it instantly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- FAIL: TestCalculatePrice
    expected 1050 (cents), got 1080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a hypothetical. Large language models hallucinate. They confidently produce code that compiles, looks reasonable, and is subtly wrong. They'll rename a variable and forget to update one reference. They'll change what a function gives back and break something three files away. They'll "simplify" a condition and break a rare scenario. The confidence is the dangerous part. The code doesn't look broken. It just is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests don't care who wrote the code
&lt;/h2&gt;

&lt;p&gt;A test doesn't know if a human or an AI wrote the function it's checking. It doesn't care. It just runs the function, compares the output to what you said it should be, and passes or fails.&lt;/p&gt;

&lt;p&gt;This makes tests the perfect safety net for vibe coding. You can let the AI rewrite entire files. You can tell it to refactor, restructure, change the architecture. As long as the tests pass, the behavior you care about is intact. The moment something breaks, you'll know immediately, not three weeks later when a user reports a weird charge on their credit card.&lt;/p&gt;

&lt;p&gt;There's a catch though.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests that spy on implementation are useless
&lt;/h2&gt;

&lt;p&gt;There are two ways to write a test. One is good. One will ruin your life.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good:&lt;/strong&gt; "I give the function these inputs, I expect this output."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bad:&lt;/strong&gt; "The function must call &lt;code&gt;database.Save()&lt;/code&gt; exactly once, then call &lt;code&gt;cache.Invalidate()&lt;/code&gt; with the argument &lt;code&gt;"users"&lt;/code&gt;, then return a struct with field &lt;code&gt;processedAt&lt;/code&gt; set to the current timestamp."&lt;/p&gt;

&lt;p&gt;The second kind tests HOW the code works internally. This seems thorough. It's actually a trap. The moment the AI (or you, or a coworker) rewrites the internals, every single one of those tests breaks, even if the behavior is perfectly correct. You moved some code into a separate place? Tests fail. You switched from one internal library to another? Tests fail. The function still does exactly what it's supposed to, but the tests don't know that because they were watching the gears, not the output.&lt;/p&gt;

&lt;p&gt;For vibe coding this is fatal. The AI rewrites internals on every prompt. If your tests check implementation details, they'll break every single time you ask the AI to change anything. You'll spend more time fixing tests than writing features. Eventually you'll start ignoring failing tests, and then you have no tests at all.&lt;/p&gt;

&lt;p&gt;Write tests that check behavior. Input in, expected output out. The internals are the AI's problem. The behavior is your contract with reality.&lt;/p&gt;

&lt;p&gt;A practical rule: if you can describe what a test checks without mentioning names of internal code parts or step-by-step details, it's a behavioral test. "An order with three items at $10 each costs $30." That sentence says nothing about implementation. Any code that makes it true is correct code.&lt;/p&gt;

&lt;h2&gt;
  
  
  How many tests do you need
&lt;/h2&gt;

&lt;p&gt;I'm honestly not sure there's a universal answer here. The standard advice is "test everything," but I think for vibe coding the priority is different. Test the things that would hurt if they broke silently. Price calculations. Authentication. Data that gets saved to a database. The core stuff.&lt;/p&gt;

&lt;p&gt;If the AI generates some utility code that formats a date for display, maybe you don't need a test for that. If it breaks, you'll see it on screen. But if it generates code that decides whether a user gets charged $50 or $500? Yeah, write a test.&lt;/p&gt;

&lt;p&gt;Start small. Five tests that cover the most important behaviors of your app. Run them after every AI-generated change. That alone puts you ahead of most vibe coders who test nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more thing
&lt;/h2&gt;

&lt;p&gt;If you're writing Go, &lt;a href="https://github.com/zerosixty/samurai" rel="noopener noreferrer"&gt;samurai&lt;/a&gt; was designed with AI-assisted development in mind. The API is one method (&lt;code&gt;s.Test()&lt;/code&gt;) - small enough to explain to a model in a single prompt - and every test runs in complete isolation, so AI-generated code with unexpected side effects can't break neighboring checks. Each test is a self-contained path with its own setup and teardown. The AI rewrites your code, you run the tests, each path either passes or fails independently. No cascading failures. Zero dependencies, parallel by default: &lt;code&gt;go get github.com/zerosixty/samurai&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Why Does Your Testing Framework Need 17 Functions?</title>
      <dc:creator>Stepan Romankov</dc:creator>
      <pubDate>Fri, 27 Feb 2026 11:42:42 +0000</pubDate>
      <link>https://forem.com/stepan_romankov/one-method-to-test-them-all-3j54</link>
      <guid>https://forem.com/stepan_romankov/one-method-to-test-them-all-3j54</guid>
      <description>&lt;p&gt;I counted Ginkgo's top-level API once. &lt;code&gt;Describe&lt;/code&gt;, &lt;code&gt;Context&lt;/code&gt;, &lt;code&gt;When&lt;/code&gt;, &lt;code&gt;It&lt;/code&gt;, &lt;code&gt;Specify&lt;/code&gt;, &lt;code&gt;By&lt;/code&gt;, &lt;code&gt;BeforeEach&lt;/code&gt;, &lt;code&gt;AfterEach&lt;/code&gt;, &lt;code&gt;BeforeAll&lt;/code&gt;, &lt;code&gt;AfterAll&lt;/code&gt;, &lt;code&gt;JustBeforeEach&lt;/code&gt;, &lt;code&gt;JustAfterEach&lt;/code&gt;, &lt;code&gt;BeforeSuite&lt;/code&gt;, &lt;code&gt;AfterSuite&lt;/code&gt;, &lt;code&gt;SynchronizedBeforeSuite&lt;/code&gt;, &lt;code&gt;SynchronizedAfterSuite&lt;/code&gt;, &lt;code&gt;DeferCleanup&lt;/code&gt;. That's 17, and I stopped counting. There are more if you include the &lt;code&gt;F&lt;/code&gt; and &lt;code&gt;P&lt;/code&gt; prefixed variants for focus and pending.&lt;/p&gt;

&lt;p&gt;GoConvey is leaner but still has &lt;code&gt;Convey&lt;/code&gt;, &lt;code&gt;So&lt;/code&gt;, &lt;code&gt;ShouldEqual&lt;/code&gt;, &lt;code&gt;SkipConvey&lt;/code&gt;, &lt;code&gt;FocusConvey&lt;/code&gt;, &lt;code&gt;Reset&lt;/code&gt;, and its own assertion DSL.&lt;/p&gt;

&lt;p&gt;I kept asking myself: what's the minimum API a scoped testing framework actually needs? Not "what's nice to have" -- what's the floor?&lt;/p&gt;

&lt;p&gt;Turns out it's one method.&lt;/p&gt;

&lt;h2&gt;
  
  
  The entire API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c"&gt;// leaf test&lt;/span&gt;
&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c"&gt;// parent with children&lt;/span&gt;
&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Skip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                     &lt;span class="c"&gt;// skip this scope&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's &lt;a href="https://github.com/zerosixty/samurai" rel="noopener noreferrer"&gt;samurai&lt;/a&gt;. The full public surface fits on a napkin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Scope&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="n"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;RunWith&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;V&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;TestScope&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;V&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c"&gt;// one method: Test()&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TestScope&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;W&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c"&gt;// alias for the common case&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;W&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;BaseContext&lt;/span&gt;       &lt;span class="c"&gt;// has Testing() and Cleanup()&lt;/span&gt;

&lt;span class="n"&gt;Sequential&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c"&gt;// option&lt;/span&gt;
&lt;span class="n"&gt;Parallel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;    &lt;span class="c"&gt;// option (default)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm not going to pretend that smaller is automatically better. Ginkgo's &lt;code&gt;BeforeAll&lt;/code&gt; exists because people need it. But I do think the Go testing ecosystem has a complexity problem, and most of that complexity exists to manage shared mutable state between tests. If you remove the shared state, the API surface collapses.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tradeoff
&lt;/h2&gt;

&lt;p&gt;Here's what samurai does that might bother you at first: the builder function runs more than once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;samurai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;samurai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;  &lt;span class="c"&gt;// this gets allocated multiple times&lt;/span&gt;

    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"with database"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;samurai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;W&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openTestDB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cleanup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;samurai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"can ping"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;samurai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;W&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NoError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Testing&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PingContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"can query"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;samurai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;W&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"SELECT 1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NoError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Testing&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two leaf tests, so the builder runs twice. Each run gets its own &lt;code&gt;db&lt;/code&gt;. The "with database" callback opens a fresh connection each time. &lt;code&gt;can ping&lt;/code&gt; and &lt;code&gt;can query&lt;/code&gt; never share a database handle.&lt;/p&gt;

&lt;p&gt;This is the point. If you run &lt;code&gt;can ping&lt;/code&gt; and &lt;code&gt;can query&lt;/code&gt; in parallel (which samurai does by default), they're operating on completely different databases. There is no race. There is no mutex. There is no "who closes the connection first" problem. The isolation comes from the execution model, not from discipline.&lt;/p&gt;

&lt;p&gt;Compare this with the &lt;code&gt;BeforeEach&lt;/code&gt; pattern where setup runs once and two siblings share the result. That works until someone adds &lt;code&gt;t.Parallel()&lt;/code&gt; and discovers the hard way that their &lt;code&gt;*sql.DB&lt;/code&gt; pointer is getting reassigned mid-query by the other goroutine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side by side
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;

&lt;span class="n"&gt;BeforeEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openTestDB&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;DeferCleanup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;It&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"can ping"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ping&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Succeed&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;It&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"can query"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT 1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NotTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HaveOccurred&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;

&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"with database"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;samurai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;W&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openTestDB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cleanup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;samurai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"can ping"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;samurai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;W&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NoError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Testing&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PingContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"can query"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;samurai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;W&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"SELECT 1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NoError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Testing&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Roughly the same line count. The difference is what happens at runtime. In the Ginkgo version, &lt;code&gt;db&lt;/code&gt; is shared. In the samurai version, each leaf gets its own &lt;code&gt;db&lt;/code&gt; because the whole closure re-runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What samurai doesn't have
&lt;/h2&gt;

&lt;p&gt;No &lt;code&gt;BeforeAll&lt;/code&gt;. If you need shared infrastructure across tests (a container, a server), set it up in &lt;code&gt;TestMain&lt;/code&gt; or at the top of your test function. The framework won't help you share state between paths because that's the thing it's designed to prevent.&lt;/p&gt;

&lt;p&gt;No built-in assertions. Use &lt;a href="https://github.com/stretchr/testify" rel="noopener noreferrer"&gt;testify&lt;/a&gt;, use &lt;a href="https://github.com/matryer/is" rel="noopener noreferrer"&gt;is&lt;/a&gt;, use &lt;code&gt;t.Errorf&lt;/code&gt;. The &lt;code&gt;RunWith&lt;/code&gt; generic variant lets you embed an assertion library into the test context so you don't have to pass &lt;code&gt;t&lt;/code&gt; everywhere, but that's optional.&lt;/p&gt;

&lt;p&gt;No &lt;code&gt;Focus&lt;/code&gt; or &lt;code&gt;Pending&lt;/code&gt; variants of &lt;code&gt;Test&lt;/code&gt;. You skip with &lt;code&gt;s.Skip()&lt;/code&gt; on the scope. You focus with &lt;code&gt;go test -run&lt;/code&gt;. Standard Go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/zerosixty/samurai
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requires Go 1.24+ (for generic type aliases and &lt;code&gt;t.Context()&lt;/code&gt;). Zero dependencies.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/zerosixty/samurai" rel="noopener noreferrer"&gt;github.com/zerosixty/samurai&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>testing</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Testing Telegram Bots in CI/CD: Zero Tokens Required</title>
      <dc:creator>Stepan Romankov</dc:creator>
      <pubDate>Mon, 26 Jan 2026 16:00:00 +0000</pubDate>
      <link>https://forem.com/stepan_romankov/testing-telegram-bots-in-cicd-zero-tokens-required-4io3</link>
      <guid>https://forem.com/stepan_romankov/testing-telegram-bots-in-cicd-zero-tokens-required-4io3</guid>
      <description>&lt;p&gt;Last month I watched a colleague's PR sit in review for three days. Not because the code was bad—it was a simple bug fix, maybe twenty lines. The problem was CI. Every time he pushed, the pipeline would run for fifteen minutes, then fail on some random test. He'd re-run it. Sometimes it passed. Sometimes it didn't. By day three, reviewers had moved on to other things.&lt;/p&gt;

&lt;p&gt;When I looked at the logs, the pattern was familiar. The tests were hitting Telegram's real API. Some failed due to rate limits. Others timed out waiting for responses. One particularly creative failure happened because the test bot's token had been rotated and nobody updated the CI secret.&lt;/p&gt;

&lt;p&gt;This is the story of how we fixed that pipeline—and how you can build a Telegram bot test suite that runs in CI/CD without any API tokens, network access, or prayers to the demo gods.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Cost of "Real" Integration Tests
&lt;/h2&gt;

&lt;p&gt;Let's talk about what happens when you test against the real Telegram API in CI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The token problem.&lt;/strong&gt; You need a bot token to talk to Telegram. Where does that token come from in CI? Usually a secret. Now you have a secret to manage. Someone rotates it and forgets to update GitHub. Someone copies the workflow file to a new repo and wonders why tests fail. Someone accidentally logs the token in a debug statement and now you're scrambling to revoke it.&lt;/p&gt;

&lt;p&gt;Secrets management sounds simple until you're debugging why your nightly build has been red for a week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The network problem.&lt;/strong&gt; CI runners live in data centers. Data centers have firewalls, proxies, and occasionally, network issues. Telegram's servers are fast, but "fast" still means 50-200ms per request. A test that sends ten messages takes at least half a second just in network latency. A test suite with a hundred such tests? You're looking at a minute of pure waiting, assuming nothing times out.&lt;/p&gt;

&lt;p&gt;And things do time out. I've seen tests that pass locally fail in CI because the runner happened to be in a region with higher latency to Telegram's servers. These failures are maddening because they're not reproducible. Run the same test again and it might pass.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rate limit problem.&lt;/strong&gt; Telegram enforces limits on how fast bots can send messages. The exact limits depend on factors Telegram doesn't fully document, but roughly: send too many messages too fast, and you get temporarily blocked. Run fifty tests in parallel, each sending a few messages, and you'll hit those limits. Now your tests fail not because of bugs, but because you're testing too enthusiastically.&lt;/p&gt;

&lt;p&gt;The "solution" most teams reach for is running tests sequentially and adding sleeps between them. This works until your test suite grows. Sequential tests with network latency and artificial delays add up fast. What started as a two-minute pipeline becomes ten minutes, then twenty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The flakiness spiral.&lt;/strong&gt; Developers stop running the full suite locally. They push and pray. CI becomes a bottleneck instead of a safety net. When a test fails, the first instinct is "re-run it" rather than "investigate it." Eventually, certain tests get marked as "known flaky" and ignored entirely. That's not testing. That's theater.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Mock Locally, Test Everything
&lt;/h2&gt;

&lt;p&gt;Here's the thing about teloxide bots: they don't actually care about Telegram. They care about an HTTP endpoint that speaks the Telegram Bot API protocol. By default, that endpoint is &lt;code&gt;api.telegram.org&lt;/code&gt;. But it doesn't have to be.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://crates.io/crates/teremock" rel="noopener noreferrer"&gt;teremock&lt;/a&gt; spins up a local HTTP server that implements 40+ Telegram Bot API methods. When you create a &lt;code&gt;MockBot&lt;/code&gt;, it configures the teloxide &lt;code&gt;Bot&lt;/code&gt; to use localhost instead of Telegram. Your handlers run exactly as they would in production—the state machine transitions, the database queries execute, the responses get sent—except there's no network call leaving the machine.&lt;/p&gt;

&lt;p&gt;If you're new to teremock, check out the &lt;a href="//./introduction.en.md"&gt;introduction article&lt;/a&gt; for the basics. This article focuses specifically on getting everything working in CI/CD.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Simplest CI Pipeline
&lt;/h2&gt;

&lt;p&gt;Let's start with a bot that doesn't use a database. Here's the complete GitHub Actions workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Swatinem/rust-cache@v2&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cargo test --workspace&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No secrets block. No environment variables. No special network configuration.&lt;/p&gt;

&lt;p&gt;Compare this to what you'd need for real API tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The old way - don't do this&lt;/span&gt;
&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;TELOXIDE_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.TELEGRAM_BOT_TOKEN }}&lt;/span&gt;
  &lt;span class="c1"&gt;# Plus handling for rate limits, retries, timeouts...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The teremock approach eliminates an entire category of CI configuration. No tokens to rotate. No secrets to manage across repositories. No "why is this environment variable not set" debugging sessions.&lt;/p&gt;

&lt;p&gt;The speed difference is dramatic too. Network round-trips that took 50-200ms each now take microseconds. A test suite that took five minutes against the real API finishes in fifteen seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding PostgreSQL for Stateful Bots
&lt;/h2&gt;

&lt;p&gt;Most production bots aren't stateless. They track conversations, store user preferences, manage orders or subscriptions. That state typically lives in PostgreSQL.&lt;/p&gt;

&lt;p&gt;GitHub Actions makes it easy to spin up a PostgreSQL service container. Here's the complete workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
          &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
          &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test_db&lt;/span&gt;
        &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
          &lt;span class="s"&gt;--health-cmd pg_isready&lt;/span&gt;
          &lt;span class="s"&gt;--health-interval 10s&lt;/span&gt;
          &lt;span class="s"&gt;--health-timeout 5s&lt;/span&gt;
          &lt;span class="s"&gt;--health-retries 5&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;5432:5432&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Swatinem/rust-cache@v2&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://postgres:postgres@localhost/test_db&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cargo test --workspace&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break down what's happening here.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;services&lt;/code&gt; block tells GitHub Actions to start a PostgreSQL container before your job runs. The container gets a health check configured so the job waits until PostgreSQL is actually ready to accept connections—no more "connection refused" race conditions.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ports&lt;/code&gt; mapping exposes PostgreSQL on the standard port 5432. From your test code's perspective, it's just a normal PostgreSQL server running on localhost.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;DATABASE_URL&lt;/code&gt; environment variable tells sqlx where to connect. This same variable works for both running migrations and for &lt;code&gt;#[sqlx::test]&lt;/code&gt; to create isolated test databases.&lt;/p&gt;

&lt;p&gt;One important detail: the health check options. Without them, your tests might start before PostgreSQL finishes initializing, leading to sporadic "connection refused" failures. The &lt;code&gt;--health-cmd pg_isready&lt;/code&gt; tells Docker to verify PostgreSQL is actually accepting connections before declaring the container healthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parallel Database Tests in CI
&lt;/h2&gt;

&lt;p&gt;If your tests use &lt;code&gt;#[serial]&lt;/code&gt; to avoid database conflicts, they'll run sequentially even in CI. That's slow. For a deep dive into parallel database testing with &lt;code&gt;#[sqlx::test]&lt;/code&gt;, see the &lt;a href="//./parallel-database-testing.en.md"&gt;parallel database testing article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The short version: &lt;code&gt;#[sqlx::test]&lt;/code&gt; creates a fresh database for each test, runs your migrations, and drops it afterward. Tests can't interfere with each other because they're literally talking to different databases. This enables full parallelism—on an 8-core CI runner, 8 tests run simultaneously.&lt;/p&gt;

&lt;p&gt;The CI workflow above already supports this. The &lt;code&gt;DATABASE_URL&lt;/code&gt; points to a PostgreSQL server where sqlx can create temporary databases. No additional configuration needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching for Faster Builds
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Swatinem/rust-cache@v2&lt;/code&gt; action caches your compiled dependencies between runs. This typically cuts build times by 60-80% on subsequent runs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Swatinem/rust-cache@v2&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cache-on-failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;cache-on-failure: true&lt;/code&gt; option is useful during development—even if tests fail, you still cache the successful compilation. This speeds up the "fix and re-run" cycle.&lt;/p&gt;

&lt;p&gt;For workspaces with multiple crates, the cache handles everything automatically. For monorepos with separate Cargo.toml files (like an &lt;code&gt;examples/&lt;/code&gt; directory), you might need additional configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Swatinem/rust-cache@v2&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;workspaces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="s"&gt;examples&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adding Clippy and Formatting Checks
&lt;/h2&gt;

&lt;p&gt;A complete CI pipeline usually includes more than just tests. Here's what the teremock repository uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
        &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
          &lt;span class="s"&gt;--health-cmd pg_isready&lt;/span&gt;
          &lt;span class="s"&gt;--health-interval 10s&lt;/span&gt;
          &lt;span class="s"&gt;--health-timeout 5s&lt;/span&gt;
          &lt;span class="s"&gt;--health-retries 5&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;5432:5432&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Swatinem/rust-cache@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cache-on-failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check formatting&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cargo fmt --all -- --check&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Clippy&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cargo clippy --workspace --all-targets -- -D warnings&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://postgres:postgres@localhost/postgres&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cargo test --workspace&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order matters here. Formatting checks are instant—if someone forgot to run &lt;code&gt;cargo fmt&lt;/code&gt;, fail fast rather than waiting for compilation. Clippy runs next because it catches issues during compilation. Tests run last since they take the longest.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;-D warnings&lt;/code&gt; flag for Clippy treats warnings as errors. This prevents "I'll fix that warning later" from becoming "that warning has been there for six months."&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment Variables and Secrets
&lt;/h2&gt;

&lt;p&gt;One of the biggest wins of mock-based testing is eliminating secrets from CI. But you might still need environment variables for other purposes—database URLs, feature flags, test configuration.&lt;/p&gt;

&lt;p&gt;For non-sensitive values, define them directly in the workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://postgres:postgres@localhost/test_db&lt;/span&gt;
  &lt;span class="na"&gt;RUST_LOG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;debug&lt;/span&gt;
  &lt;span class="na"&gt;MY_FEATURE_FLAG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;enabled&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For values that differ between environments, use GitHub's environment variables feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.DATABASE_URL }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cargo test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the difference: &lt;code&gt;secrets.X&lt;/code&gt; for sensitive values, &lt;code&gt;vars.X&lt;/code&gt; for non-sensitive configuration. Using &lt;code&gt;vars&lt;/code&gt; instead of &lt;code&gt;secrets&lt;/code&gt; means values show up in logs, making debugging easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Test Failures
&lt;/h2&gt;

&lt;p&gt;When tests fail in CI, you need enough information to debug without re-running locally. A few techniques help:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Preserve test output.&lt;/strong&gt; By default, Cargo captures test output and only shows it for failures. The &lt;code&gt;--nocapture&lt;/code&gt; flag shows all output, but that's usually too noisy. Instead, use &lt;code&gt;RUST_LOG&lt;/code&gt; to control verbosity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;RUST_LOG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my_bot=debug&lt;/span&gt;
    &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://postgres:postgres@localhost/test_db&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cargo test --workspace&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Upload artifacts on failure.&lt;/strong&gt; If your tests generate logs or other artifacts, upload them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload logs on failure&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;failure()&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test-logs&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;target/test-logs/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Matrix testing for multiple Rust versions.&lt;/strong&gt; If you support multiple Rust versions, test them all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;rust&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;stable&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;beta&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;1.83&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dtolnay/rust-toolchain@master&lt;/span&gt;
    &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;toolchain&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.rust }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Complete Production Pipeline
&lt;/h2&gt;

&lt;p&gt;Putting it all together, here's a battle-tested CI configuration for a Telegram bot with database persistence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;CARGO_TERM_COLOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ci&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
          &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
          &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
        &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
          &lt;span class="s"&gt;--health-cmd pg_isready&lt;/span&gt;
          &lt;span class="s"&gt;--health-interval 10s&lt;/span&gt;
          &lt;span class="s"&gt;--health-timeout 5s&lt;/span&gt;
          &lt;span class="s"&gt;--health-retries 5&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;5432:5432&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Swatinem/rust-cache@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cache-on-failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check formatting&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cargo fmt --all -- --check&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Clippy&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cargo clippy --workspace --all-targets -- -D warnings&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://postgres:postgres@localhost/postgres&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cargo test --workspace&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload test artifacts on failure&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;failure()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test-output&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;target/&lt;/span&gt;
          &lt;span class="na"&gt;retention-days&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Starts PostgreSQL before tests run&lt;/li&gt;
&lt;li&gt;Caches dependencies for faster subsequent runs&lt;/li&gt;
&lt;li&gt;Fails fast on formatting issues&lt;/li&gt;
&lt;li&gt;Catches lint warnings with Clippy&lt;/li&gt;
&lt;li&gt;Runs all tests in parallel with database isolation&lt;/li&gt;
&lt;li&gt;Preserves artifacts if something fails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total runtime on a warm cache: 2-3 minutes. Most of that is compilation. The tests themselves take seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Don't Need Anymore
&lt;/h2&gt;

&lt;p&gt;Let me be explicit about what this approach eliminates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;TELOXIDE_TOKEN&lt;/code&gt; secret.&lt;/strong&gt; The mock server doesn't need a real token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No retry logic for flaky network tests.&lt;/strong&gt; There's no network to be flaky.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No rate limit handling.&lt;/strong&gt; The mock server doesn't rate limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;--test-threads=1&lt;/code&gt; to avoid conflicts.&lt;/strong&gt; Database isolation handles that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No "known flaky" test annotations.&lt;/strong&gt; Tests either pass or fail deterministically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No special network configuration.&lt;/strong&gt; Everything runs on localhost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pipeline my colleague was fighting with? After migrating to this approach, it went from fifteen minutes with random failures to three minutes with consistent passes. The PR that sat in review for three days would have merged in an hour.&lt;/p&gt;

&lt;p&gt;That's the goal: CI that helps you ship faster, not CI that becomes another thing to debug.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/zerosixty/teremock" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://crates.io/crates/teremock" rel="noopener noreferrer"&gt;Crates.io&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  - &lt;a href="https://docs.rs/teremock" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;If you're setting up CI for a Telegram bot and run into issues, open an issue on GitHub. I've debugged more pipeline configurations than I'd like to admit, and I'm happy to help.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>testing</category>
      <category>opensource</category>
      <category>telegram</category>
    </item>
    <item>
      <title>From 5 Minutes to 15 Seconds: Parallel Database Tests for Telegram Bots</title>
      <dc:creator>Stepan Romankov</dc:creator>
      <pubDate>Fri, 23 Jan 2026 08:25:56 +0000</pubDate>
      <link>https://forem.com/stepan_romankov/from-5-minutes-to-15-seconds-parallel-database-tests-for-telegram-bots-4a50</link>
      <guid>https://forem.com/stepan_romankov/from-5-minutes-to-15-seconds-parallel-database-tests-for-telegram-bots-4a50</guid>
      <description>&lt;p&gt;Your test suite takes 5 minutes to run. You make a small change, hit &lt;code&gt;cargo test&lt;/code&gt;, and wait. And wait. You check Twitter. Still waiting. By the time tests finish, you've forgotten what you were working on.&lt;/p&gt;

&lt;p&gt;I've been there. My phrase_bot had 10 database tests. Each one needed a clean database. The "safe" solution? Run them sequentially with &lt;code&gt;#[serial]&lt;/code&gt;. The result? A coffee break every time I ran the suite.&lt;/p&gt;

&lt;p&gt;Then I discovered how to combine &lt;code&gt;#[sqlx::test]&lt;/code&gt; with teremock and everything changed. Same tests, but running in parallel with complete isolation. No cleanup code. No flaky failures. No more coffee breaks.&lt;/p&gt;

&lt;p&gt;This article walks through why traditional database testing is painful, how isolation-based testing solves it, and how to set it up with teremock for your Telegram bot. By the end, you'll have a pattern that scales to hundreds of tests without slowing down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pain of Sequential Database Tests
&lt;/h2&gt;

&lt;p&gt;Let's talk about what most of us do when we first add database tests to a project.&lt;/p&gt;

&lt;p&gt;You write your first test. It creates a user, checks something, passes. Great. You write a second test. It also creates a user with the same ID because you copied the first test. Now you have a primary key conflict. Tests fail randomly depending on execution order.&lt;/p&gt;

&lt;p&gt;The "obvious" fix is to clean up the database before each test. You write a helper function that deletes everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;cleanup_database&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;PgPool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;sqlx&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DELETE FROM phrases"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nn"&gt;sqlx&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DELETE FROM users"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[tokio::test]&lt;/span&gt;
&lt;span class="nd"&gt;#[serial]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;test_create_user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_test_pool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;cleanup_database&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ... test code&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works until it doesn't. And it stops working in subtle, frustrating ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ordering problem.&lt;/strong&gt; You add a &lt;code&gt;phrases&lt;/code&gt; table with a foreign key to &lt;code&gt;users&lt;/code&gt;. Now &lt;code&gt;DELETE FROM users&lt;/code&gt; fails because phrases reference those users. You need to delete phrases first. Every schema change potentially breaks your cleanup function. These bugs don't appear immediately — they appear weeks later when someone adds a foreign key and forgets to update the cleanup order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The forgotten table problem.&lt;/strong&gt; You add a &lt;code&gt;settings&lt;/code&gt; table. You forget to add it to cleanup. Tests pass locally because you're running them in a certain order. CI runs them differently. Random failures. You spend an hour debugging before realizing the cleanup is incomplete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The performance problem.&lt;/strong&gt; To avoid all these race conditions, you slap &lt;code&gt;#[serial]&lt;/code&gt; on every test. Now they run one at a time. Ten tests at 500ms each is 5 seconds. Fifty tests is 25 seconds. A hundred tests is almost a minute. You stop running the full suite. You start running "just the tests for this file." Bugs slip through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The CI flakiness problem.&lt;/strong&gt; Even with &lt;code&gt;#[serial]&lt;/code&gt;, I've seen tests that pass locally and fail in GitHub Actions. Different machines, different timing, different connection pooling behavior. The database connection from the previous test hasn't fully closed yet. Or migrations run in a weird order. These are the worst bugs because they're not reproducible locally.&lt;/p&gt;

&lt;p&gt;The fundamental issue is that we're trying to reuse a shared resource (the database) across tests that should be independent. We're fighting the architecture instead of working with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Isolation Insight
&lt;/h2&gt;

&lt;p&gt;What if instead of cleaning a shared database, each test got its own database?&lt;/p&gt;

&lt;p&gt;Think about it. Test A runs against &lt;code&gt;test_db_abc123&lt;/code&gt;. Test B runs against &lt;code&gt;test_db_def456&lt;/code&gt;. They can't possibly interfere with each other. They can run in parallel. No cleanup needed — just drop the database when the test ends.&lt;/p&gt;

&lt;p&gt;This sounds expensive. Creating and dropping databases has overhead, right?&lt;/p&gt;

&lt;p&gt;Yes, but less than you'd think. PostgreSQL can create an empty database in about 50-100ms. That's nothing compared to the time you save by running tests in parallel. If you have 20 tests that each take 500ms, sequential execution takes 10 seconds. Parallel execution with 100ms overhead per test? Under 2 seconds on a multi-core machine.&lt;/p&gt;

&lt;p&gt;This is exactly what &lt;code&gt;#[sqlx::test]&lt;/code&gt; does. It's a macro from the sqlx crate that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Creates a fresh database with a unique name before your test runs&lt;/li&gt;
&lt;li&gt;Runs your migrations automatically&lt;/li&gt;
&lt;li&gt;Passes a connection pool to your test function&lt;/li&gt;
&lt;li&gt;Drops the database after the test completes (pass or fail)
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[sqlx::test(migrator&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"crate::db::MIGRATOR"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;test_create_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PgPool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// This pool connects to a brand new database&lt;/span&gt;
    &lt;span class="c1"&gt;// Named something like: myapp_test_a7f3b2c1&lt;/span&gt;
    &lt;span class="c1"&gt;// No other test can see this data&lt;/span&gt;
    &lt;span class="c1"&gt;// Dropped automatically when the test ends&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The beauty is in what you &lt;em&gt;don't&lt;/em&gt; have to write. No cleanup functions. No &lt;code&gt;#[serial]&lt;/code&gt;. No careful ordering of deletions. The isolation is handled at the database level, which is exactly where it should be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrating with teremock
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting for Telegram bot developers. teremock already gives you isolated bot instances — each &lt;code&gt;MockBot&lt;/code&gt; has its own mock server on a random port. Combining that with &lt;code&gt;#[sqlx::test]&lt;/code&gt; gives you complete test isolation: isolated bot, isolated database, fully parallel execution.&lt;/p&gt;

&lt;p&gt;The setup requires two pieces. First, you need to export your migrator so sqlx can find your migrations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/db/mod.rs&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;MIGRATOR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;sqlx&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;migrate&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Migrator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;sqlx&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;migrate!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"./migrations"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a static reference to your migrations directory. The &lt;code&gt;sqlx::migrate!&lt;/code&gt; macro reads the migration files at compile time, so there's no runtime file system access.&lt;/p&gt;

&lt;p&gt;Second, you write your tests using &lt;code&gt;#[sqlx::test]&lt;/code&gt; instead of &lt;code&gt;#[tokio::test]&lt;/code&gt;, and you inject the pool into your MockBot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[sqlx::test(migrator&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"crate::db::MIGRATOR"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;test_start_creates_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PgPool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;MockBot&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nn"&gt;MockMessageText&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/start"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;handler_tree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// This is the critical line — inject the isolated database&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.dependencies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;dptree&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;deps!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;

    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.dispatch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;db&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;MockUser&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;i64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nd"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="py"&gt;.nickname&lt;/span&gt;&lt;span class="nf"&gt;.is_none&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;pool&lt;/code&gt; parameter is your isolated database. When you inject it into the bot's dependencies, all your handlers that expect a &lt;code&gt;PgPool&lt;/code&gt; will receive this isolated instance. Your handler code doesn't change at all — it still accepts &lt;code&gt;pool: PgPool&lt;/code&gt; and runs queries against it. But in tests, that pool points to a database that exists only for this one test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Complex Flows
&lt;/h2&gt;

&lt;p&gt;The real payoff comes when you're testing multi-step dialogues with database interactions.&lt;/p&gt;

&lt;p&gt;My phrase_bot lets users create custom phrases through a conversation. The user clicks "Add phrase," enters an emoji, enters a trigger word, enters a response template, and the bot saves it to the database. Testing this requires both dialogue state and database state to work together.&lt;/p&gt;

&lt;p&gt;With the old approach, I'd need to carefully set up the database, run through the dialogue, verify the result, then clean up. If another test also created phrases, I'd need &lt;code&gt;#[serial]&lt;/code&gt; to prevent conflicts. The test file became a minefield of ordering dependencies.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;#[sqlx::test]&lt;/code&gt;, each test is completely self-contained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[sqlx::test(migrator&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"crate::db::MIGRATOR"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;test_add_phrase_flow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PgPool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Create the user this test needs&lt;/span&gt;
    &lt;span class="nn"&gt;db&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;MockUser&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;i64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;MockBot&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nn"&gt;MockMessageText&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/start"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;handler_tree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.dependencies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;deps!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;get_test_storage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;

    &lt;span class="c1"&gt;// Walk through the dialogue&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.dispatch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;MockMessageText&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Add phrase"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.dispatch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// ... user enters emoji, trigger text, response template ...&lt;/span&gt;

    &lt;span class="c1"&gt;// Verify it saved to OUR database (not shared with anyone)&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;phrases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;db&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get_user_phrases&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;MockUser&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;i64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phrases&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="py"&gt;.emoji&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"🤗"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now I can write a deletion test that also uses the same user ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[sqlx::test(migrator&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"crate::db::MIGRATOR"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;test_delete_phrase_flow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PgPool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Set up a phrase to delete&lt;/span&gt;
    &lt;span class="nn"&gt;db&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;MockUser&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;i64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nn"&gt;db&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create_phrase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;MockUser&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;i64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"🤗"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"hug"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;MockBot&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.dependencies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;deps!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;get_test_storage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;

    &lt;span class="c1"&gt;// ... navigate to delete, confirm deletion ...&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;phrases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;db&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get_user_phrases&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;MockUser&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;i64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nd"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phrases&lt;/span&gt;&lt;span class="nf"&gt;.is_empty&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both tests use &lt;code&gt;MockUser::ID&lt;/code&gt;. Both manipulate phrases. With &lt;code&gt;#[serial]&lt;/code&gt; and shared databases, that's a conflict waiting to happen. With &lt;code&gt;#[sqlx::test]&lt;/code&gt;, they run in parallel without a care in the world. Different databases, no interference.&lt;/p&gt;

&lt;p&gt;This changes how you think about test design. You stop worrying about "what state will the database be in when this test runs?" Each test defines its own preconditions. Each test is a fresh start. The cognitive load drops dramatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance: The Numbers
&lt;/h2&gt;

&lt;p&gt;I ran benchmarks on my phrase_bot test suite to quantify the difference. Here's what I found:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;8 tests&lt;/th&gt;
&lt;th&gt;20 tests&lt;/th&gt;
&lt;th&gt;50 tests&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;#[serial]&lt;/code&gt; + cleanup&lt;/td&gt;
&lt;td&gt;4.2s&lt;/td&gt;
&lt;td&gt;10.5s&lt;/td&gt;
&lt;td&gt;26.3s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;#[sqlx::test]&lt;/code&gt; parallel&lt;/td&gt;
&lt;td&gt;1.1s&lt;/td&gt;
&lt;td&gt;1.8s&lt;/td&gt;
&lt;td&gt;3.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The speedup is &lt;strong&gt;4-8x&lt;/strong&gt;, and the gap widens as you add more tests.&lt;/p&gt;

&lt;p&gt;Why? Sequential execution scales linearly with test count. If each test takes 500ms, 50 tests take 25 seconds. There's no way around it.&lt;/p&gt;

&lt;p&gt;Parallel execution scales with your CPU cores. On my 8-core machine, 8 tests that each take 500ms complete in roughly 600ms (500ms for the test plus ~100ms database overhead). The marginal cost of additional tests is just the overhead of creating the database, not the execution time.&lt;/p&gt;

&lt;p&gt;The practical impact is huge. 26 seconds is "I'll check my phone while this runs." 3 seconds is "already done before I looked away." When tests are fast, you run them more often. When you run them more often, you catch bugs earlier. Earlier bugs are cheaper to fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas and Solutions
&lt;/h2&gt;

&lt;p&gt;After using this pattern for several months, I've hit a few recurring issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting to inject the pool&lt;/strong&gt; is the most common mistake. Your test runs, your bot dispatches, and nothing happens. No error, just silence. The handler tried to access the database, got a different pool (or none), and quietly failed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wrong: bot has no access to your test database&lt;/span&gt;
&lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.dispatch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Right: inject dependencies BEFORE dispatch&lt;/span&gt;
&lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.dependencies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;deps!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;
&lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.dispatch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make this a habit: &lt;code&gt;MockBot::new()&lt;/code&gt;, then &lt;code&gt;dependencies()&lt;/code&gt;, then &lt;code&gt;dispatch()&lt;/code&gt;. Every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connection limits&lt;/strong&gt; can bite you with large test suites. PostgreSQL has a default &lt;code&gt;max_connections&lt;/code&gt; setting (usually 100). If you're running 50 tests in parallel, each with its own connection pool, you might exceed that limit.&lt;/p&gt;

&lt;p&gt;Solutions, in order of preference:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use PgBouncer for connection pooling&lt;/li&gt;
&lt;li&gt;Increase &lt;code&gt;max_connections&lt;/code&gt; in postgresql.conf&lt;/li&gt;
&lt;li&gt;Limit test parallelism: &lt;code&gt;cargo test -- --test-threads=4&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Missing DATABASE_URL&lt;/strong&gt; is another gotcha. &lt;code&gt;#[sqlx::test]&lt;/code&gt; needs to know where to create test databases. It reads &lt;code&gt;DATABASE_URL&lt;/code&gt; from the environment and uses that server to create/drop the temporary databases. Make sure your &lt;code&gt;.env&lt;/code&gt; file has it set, or export it in your shell before running tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Not to Use This
&lt;/h2&gt;

&lt;p&gt;No technique is universally applicable. Here's when you might skip &lt;code&gt;#[sqlx::test]&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Very small test suites.&lt;/strong&gt; If you have 3 tests that run in 200ms total, the overhead of creating databases isn't worth it. Just use &lt;code&gt;#[serial]&lt;/code&gt; and move on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tests that intentionally share state.&lt;/strong&gt; Sometimes you want test B to see data created by test A. This is rare and often a code smell, but if you genuinely need it, isolated databases won't work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQLite in-memory databases.&lt;/strong&gt; SQLite's in-memory mode creates a new database per connection automatically. You already have isolation. &lt;code&gt;#[sqlx::test]&lt;/code&gt; still works, but you're not gaining much.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tests hitting external services.&lt;/strong&gt; If your tests call real APIs (payment processors, third-party services), parallel execution might trigger rate limits. Though arguably, those calls should be mocked anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the Switch
&lt;/h2&gt;

&lt;p&gt;If you're convinced and want to migrate an existing test suite, here's the process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Add the migrator export.&lt;/strong&gt; Create &lt;code&gt;pub static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("./migrations");&lt;/code&gt; in your database module.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ensure DATABASE_URL is set.&lt;/strong&gt; Add it to &lt;code&gt;.env&lt;/code&gt; or your CI environment variables.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Convert tests one at a time.&lt;/strong&gt; Replace &lt;code&gt;#[tokio::test]&lt;/code&gt; with &lt;code&gt;#[sqlx::test(migrator = "crate::db::MIGRATOR")]&lt;/code&gt;. Add &lt;code&gt;pool: PgPool&lt;/code&gt; as the first parameter. Add &lt;code&gt;bot.dependencies(deps![pool.clone()])&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Remove cleanup code.&lt;/strong&gt; Delete your &lt;code&gt;cleanup_database()&lt;/code&gt; function. Delete &lt;code&gt;#[serial]&lt;/code&gt; attributes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run and verify.&lt;/strong&gt; Run &lt;code&gt;cargo test&lt;/code&gt; and watch them execute in parallel. Check that all tests still pass.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The conversion is mechanical. Most tests require changing 3-4 lines. The payoff is immediate — your test suite gets faster with every test you convert.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;The combination of teremock for bot mocking and &lt;code&gt;#[sqlx::test]&lt;/code&gt; for database isolation transformed my testing workflow. What used to take 5 minutes now takes 15 seconds. What used to fail randomly in CI now passes reliably every time.&lt;/p&gt;

&lt;p&gt;But the real win isn't the speed. It's the simplicity. No cleanup functions to maintain. No ordering dependencies to track. No &lt;code&gt;#[serial]&lt;/code&gt; attributes to remember. Each test is an island, complete and self-sufficient.&lt;/p&gt;

&lt;p&gt;When the feedback loop is fast, you test more. When tests are simple, you write more of them. When you have more tests, you ship fewer bugs.&lt;/p&gt;

&lt;p&gt;That's the goal, isn't it?&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/zerosixty/teremock" rel="noopener noreferrer"&gt;https://github.com/zerosixty/teremock&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crates.io:&lt;/strong&gt; &lt;a href="https://crates.io/crates/teremock" rel="noopener noreferrer"&gt;https://crates.io/crates/teremock&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation:&lt;/strong&gt; &lt;a href="https://docs.rs/teremock" rel="noopener noreferrer"&gt;https://docs.rs/teremock&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Acknowledgments
&lt;/h2&gt;

&lt;p&gt;The approach to mock testing for teloxide bots was pioneered by the &lt;a href="https://github.com/LasterAlex/teloxide_tests" rel="noopener noreferrer"&gt;teloxide_tests&lt;/a&gt; project, which served as a major source of inspiration for teremock's architecture and design patterns.&lt;/p&gt;

&lt;p&gt;Special thanks to the &lt;a href="https://github.com/teloxide/teloxide" rel="noopener noreferrer"&gt;teloxide&lt;/a&gt; team for building such an excellent framework. Without their work, the Rust Telegram bot ecosystem would be a very different place.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If this article was helpful, consider starring the repo on GitHub. Found a bug or have a question? Open an issue and we'll figure it out together.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>telegram</category>
      <category>testing</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How I Stopped Worrying and Started Testing My Telegram Bots</title>
      <dc:creator>Stepan Romankov</dc:creator>
      <pubDate>Wed, 21 Jan 2026 12:40:38 +0000</pubDate>
      <link>https://forem.com/stepan_romankov/how-i-stopped-worrying-and-started-testing-my-telegram-bots-2418</link>
      <guid>https://forem.com/stepan_romankov/how-i-stopped-worrying-and-started-testing-my-telegram-bots-2418</guid>
      <description>&lt;p&gt;&lt;em&gt;A story about testing Telegram bots without the pain&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Have you ever shipped a Telegram bot and immediately regretted it? Maybe your &lt;code&gt;/start&lt;/code&gt; command crashed spectacularly at 3 AM, or that callback button you "definitely tested" decided to ghost your users. I've been there. Testing Telegram bots traditionally meant one of two things: manually clicking through your bot like a QA intern, or setting up elaborate integration tests that require actual API tokens and network access.&lt;/p&gt;

&lt;p&gt;Neither is fun. Neither scales. And both make CI pipelines cry.&lt;/p&gt;

&lt;p&gt;That's why I built &lt;strong&gt;teremock&lt;/strong&gt; — a testing library that lets you write fast, reliable tests for your &lt;a href="https://github.com/teloxide/teloxide" rel="noopener noreferrer"&gt;teloxide&lt;/a&gt; bots without ever hitting the real Telegram API.&lt;/p&gt;

&lt;p&gt;Let me show you what I mean.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Testing Telegram Bots
&lt;/h2&gt;

&lt;p&gt;Picture this: you've got a calculator bot. Users send &lt;code&gt;/start&lt;/code&gt;, click a button to add or subtract, enter two numbers, and get a result. Simple enough. But how do you test it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1: Manual testing.&lt;/strong&gt; You open Telegram, type commands, click buttons, and hope everything works. Rinse and repeat after every code change. This doesn't scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2: Real API testing.&lt;/strong&gt; You set up a test bot token, hit the actual Telegram servers, and pray your internet is stable. Tests take forever because network requests aren't exactly speedy. Good luck running this in CI without exposing credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 3: Mock everything yourself.&lt;/strong&gt; You spend more time building test infrastructure than actual features. Eventually, you question your life choices.&lt;/p&gt;

&lt;p&gt;There had to be a better way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter teremock
&lt;/h2&gt;

&lt;p&gt;teremock (&lt;strong&gt;Te&lt;/strong&gt;legram · &lt;strong&gt;Re&lt;/strong&gt;alistic · &lt;strong&gt;Mock&lt;/strong&gt;ing) takes a different approach. It spins up a lightweight mock server that pretends to be the Telegram Bot API. Your bot talks to this server instead of the real one. From your bot's perspective, nothing changes — it's making the same API calls it always does. But now those calls are instant, offline, and completely under your control.&lt;/p&gt;

&lt;p&gt;Here's the simplest possible test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;teremock&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;MockBot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MockMessageText&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nd"&gt;#[tokio::test]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;test_hello_world&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Create a mock message (as if a user sent "Hi!")&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;mock_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;MockMessageText&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hi!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Create a bot with your handler tree&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;MockBot&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mock_message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;handler_tree&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Dispatch the update through your handlers&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.dispatch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Check what your bot sent back&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.get_responses&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="py"&gt;.sent_messages&lt;/span&gt;&lt;span class="nf"&gt;.last&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hello World!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No API tokens. No network. No waiting. Just fast, deterministic tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Build Something Real
&lt;/h2&gt;

&lt;p&gt;Enough theory. Let's test an actual stateful bot — a simple calculator that walks users through adding or subtracting numbers.&lt;/p&gt;

&lt;p&gt;First, here's the handler setup (the part you'd normally write anyway):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;teloxide&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;
    &lt;span class="nn"&gt;dispatching&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="nn"&gt;dialogue&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;InMemStorage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UpdateFilterExt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UpdateHandler&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nn"&gt;dptree&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nn"&gt;prelude&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nd"&gt;#[derive(Clone,&lt;/span&gt; &lt;span class="nd"&gt;Default)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;#[default]&lt;/span&gt;
    &lt;span class="n"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;AwaitingFirstNumber&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;AwaitingSecondNumber&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;i64&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;MyDialogue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Dialogue&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;InMemStorage&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;handler_tree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;UpdateHandler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Send&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Sync&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;'static&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;dptree&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.branch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;filter_message&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="py"&gt;.enter_dialogue&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;InMemStorage&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="c1"&gt;// ... your handler branches here&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the fun part — testing the entire conversation flow in one test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;teremock&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;MockBot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MockCallbackQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MockMessageText&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;teloxide&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;dptree&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;#[tokio::test]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;test_full_addition_flow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Start with /start command&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;MockBot&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nn"&gt;MockMessageText&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/start"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;handler_tree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Inject your storage dependency&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.dependencies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;deps!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nn"&gt;InMemStorage&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;

    &lt;span class="c1"&gt;// User sends /start&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.dispatch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.get_responses&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="py"&gt;.sent_messages&lt;/span&gt;&lt;span class="nf"&gt;.last&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"What do you want to do?"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// User clicks the "add" button&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;MockCallbackQuery&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"add"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.dispatch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.get_responses&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="py"&gt;.sent_messages&lt;/span&gt;&lt;span class="nf"&gt;.last&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Enter the first number"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// User enters first number&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;MockMessageText&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"5"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.dispatch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.get_responses&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="py"&gt;.sent_messages&lt;/span&gt;&lt;span class="nf"&gt;.last&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Enter the second number"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// User enters second number&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;MockMessageText&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"4"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.dispatch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.get_responses&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="py"&gt;.sent_messages&lt;/span&gt;&lt;span class="nf"&gt;.last&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Your result: 9"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what's happening here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One test, full conversation.&lt;/strong&gt; No need to split your flow into five separate tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Natural state transitions.&lt;/strong&gt; The dialogue state updates through your actual handlers, not manual manipulation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real dependency injection.&lt;/strong&gt; Your &lt;code&gt;InMemStorage&lt;/code&gt; works exactly like in production.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What About Edge Cases?
&lt;/h2&gt;

&lt;p&gt;Great bots handle weird inputs gracefully. Let's test that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[tokio::test]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;test_invalid_number_input&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;MockBot&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nn"&gt;MockMessageText&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/start"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;handler_tree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.dependencies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;deps!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nn"&gt;InMemStorage&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;

    &lt;span class="c1"&gt;// Get to the "enter first number" state&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.dispatch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;MockCallbackQuery&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"add"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.dispatch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// User sends garbage instead of a number&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;MockMessageText&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"not a number"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.dispatch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.get_responses&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="py"&gt;.sent_messages&lt;/span&gt;&lt;span class="nf"&gt;.last&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Please enter a valid number"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// User sends a photo for some reason&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;MockMessagePhoto&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.dispatch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.get_responses&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="py"&gt;.sent_messages&lt;/span&gt;&lt;span class="nf"&gt;.last&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Please send text"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// User finally sends a valid number&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;MockMessageText&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"5"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.dispatch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.get_responses&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="py"&gt;.sent_messages&lt;/span&gt;&lt;span class="nf"&gt;.last&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Enter the second number"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test covers three scenarios in one function: invalid text, wrong message type, and recovery. Your error handling actually gets tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  Digging Deeper: Request Inspection
&lt;/h2&gt;

&lt;p&gt;Sometimes you need to verify more than just the message text. Maybe you're testing that your bot uses the right parse mode, or that a photo is marked as a spoiler. teremock gives you full access to both the sent message &lt;em&gt;and&lt;/em&gt; the original request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[tokio::test]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;test_message_formatting&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;MockBot&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nn"&gt;MockMessageText&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/styled"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;handler_tree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.dispatch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.get_responses&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Check the message content&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="py"&gt;.sent_messages_text&lt;/span&gt;&lt;span class="nf"&gt;.last&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="py"&gt;.message&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;b&amp;gt;Bold&amp;lt;/b&amp;gt; text"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// Verify the parse mode in the original request&lt;/span&gt;
    &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="py"&gt;.bot_request.parse_mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ParseMode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Html&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For media messages, this becomes even more useful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[tokio::test]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;test_photo_with_spoiler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;MockBot&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nn"&gt;MockMessageText&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/secret_photo"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;handler_tree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.dispatch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;photo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="nf"&gt;.get_responses&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="py"&gt;.sent_messages_photo&lt;/span&gt;&lt;span class="nf"&gt;.last&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;photo&lt;/span&gt;&lt;span class="py"&gt;.message&lt;/span&gt;&lt;span class="nf"&gt;.caption&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Mystery image!"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nd"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;photo&lt;/span&gt;&lt;span class="py"&gt;.bot_request.has_spoiler&lt;/span&gt;&lt;span class="nf"&gt;.unwrap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Performance Story
&lt;/h2&gt;

&lt;p&gt;Here's where teremock really shines. The mock server starts once when you create a &lt;code&gt;MockBot&lt;/code&gt; and persists across all your dispatches. No server restart between interactions.&lt;/p&gt;

&lt;p&gt;The numbers speak for themselves:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;teremock&lt;/th&gt;
&lt;th&gt;Server-per-dispatch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;50 sequential dispatches&lt;/td&gt;
&lt;td&gt;~2 seconds&lt;/td&gt;
&lt;td&gt;~30-60 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's &lt;strong&gt;15-30x faster&lt;/strong&gt; for comprehensive test suites. And because each dispatch runs in its own tokio task, you won't hit stack overflow issues even with dozens of interactions in a single test.&lt;/p&gt;

&lt;p&gt;Your CI pipeline will thank you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Under the Hood?
&lt;/h2&gt;

&lt;p&gt;teremock supports 40+ Telegram Bot API methods out of the box:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Messages:&lt;/strong&gt; sendMessage, sendPhoto, sendVideo, sendAudio, sendVoice, sendDocument, sendAnimation, sendSticker, sendLocation, sendVenue, sendContact, sendPoll, sendDice, sendInvoice, sendMediaGroup, sendChatAction...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Editing:&lt;/strong&gt; editMessageText, editMessageCaption, editMessageReplyMarkup&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Management:&lt;/strong&gt; deleteMessage, forwardMessage, copyMessage, pinChatMessage, unpinChatMessage...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Callbacks &amp;amp; More:&lt;/strong&gt; answerCallbackQuery, setMessageReaction, setMyCommands, getFile, getMe...&lt;/p&gt;

&lt;p&gt;All the builders follow a fluent pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Text message with custom sender&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;MockMessageText&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hello from a specific user"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;MockUser&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.first_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Alex"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// Callback query with specific data&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;MockCallbackQuery&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"button_clicked"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;MockUser&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// Photo message&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;photo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;MockMessagePhoto&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.caption&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Check this out!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Add teremock to your dev dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dev-dependencies]&lt;/span&gt;
&lt;span class="py"&gt;teremock&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.5"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you're ready to go. Works with &lt;code&gt;#[tokio::test]&lt;/code&gt; out of the box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/zerosixty/teremock" rel="noopener noreferrer"&gt;https://github.com/zerosixty/teremock&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Crates.io: &lt;a href="https://crates.io/crates/teremock" rel="noopener noreferrer"&gt;https://crates.io/crates/teremock&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Documentation: &lt;a href="https://docs.rs/teremock" rel="noopener noreferrer"&gt;https://docs.rs/teremock&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repository includes several example bots with full test suites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;hello_world_bot&lt;/code&gt; — The basics&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;calculator_bot&lt;/code&gt; — Stateful dialogues with callbacks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;album_bot&lt;/code&gt; — Media group handling&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;file_download_bot&lt;/code&gt; — File operations&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;phrase_bot&lt;/code&gt; — Database integration patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Testing Telegram bots doesn't have to be painful. With teremock, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write tests that run in milliseconds, not minutes&lt;/li&gt;
&lt;li&gt;Test complete multi-step conversations in single test functions&lt;/li&gt;
&lt;li&gt;Verify your bot's behavior without network access or API tokens&lt;/li&gt;
&lt;li&gt;Catch edge cases before your users do&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The days of manual Telegram testing or flaky network-dependent CI are over.&lt;/p&gt;




&lt;h2&gt;
  
  
  Acknowledgments
&lt;/h2&gt;

&lt;p&gt;teremock builds upon ideas from &lt;a href="https://github.com/LasterAlex/teloxide_tests" rel="noopener noreferrer"&gt;teloxide_tests&lt;/a&gt; by LasterAlex, which pioneered the concept of mock testing for teloxide bots. That project was a major source of inspiration for this library's approach.&lt;/p&gt;

&lt;p&gt;A huge thank you to the &lt;a href="https://github.com/teloxide/teloxide" rel="noopener noreferrer"&gt;teloxide&lt;/a&gt; team for building such an excellent Telegram bot framework. Their work makes building Telegram bots in Rust an absolute joy.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Happy testing!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>telegram</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
