<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alex</title>
    <description>The latest articles on DEV Community by Alex (@nerjs).</description>
    <link>https://dev.to/nerjs</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%2F3976751%2F56709818-35cc-4274-9e0f-acbb95fe48dc.png</url>
      <title>DEV Community: Alex</title>
      <link>https://dev.to/nerjs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nerjs"/>
    <language>en</language>
    <item>
      <title>Testing in Rust: from cargo test to mocking HTTP calls</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Wed, 10 Jun 2026 01:57:18 +0000</pubDate>
      <link>https://dev.to/nerjs/testing-in-rust-from-cargo-test-to-mocking-http-calls-1lpg</link>
      <guid>https://dev.to/nerjs/testing-in-rust-from-cargo-test-to-mocking-http-calls-1lpg</guid>
      <description>&lt;p&gt;People love to repeat that in Rust "if it compiles, it works". The compiler does kill a whole class of bugs, but it doesn't check your logic. A wrong discount calculation compiles just fine. So does a mixed-up header in a request to a payment API. You still need tests, and writing them in Rust is easy enough: the runner is built into the toolchain, no jest/pytest to install.&lt;/p&gt;

&lt;p&gt;A quick look at the basics first, then the painful part: testing code that talks HTTP.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the language gives you
&lt;/h2&gt;

&lt;p&gt;A minimal test with zero dependencies:&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;#[cfg(test)]&lt;/span&gt;
&lt;span class="k"&gt;mod&lt;/span&gt; &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="k"&gt;super&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="nd"&gt;#[test]&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;discount_is_applied&lt;/span&gt;&lt;span class="p"&gt;()&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="nf"&gt;apply_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;90&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;&lt;code&gt;cargo test&lt;/code&gt; and you're done. Unit tests live next to the code and can see private functions. Integration tests go into the &lt;code&gt;tests/&lt;/code&gt; directory and treat the crate as an external consumer, public API only.&lt;/p&gt;

&lt;p&gt;And then there are doc tests. Code examples in your docs get compiled and executed on every run. An outdated example in the README simply won't pass CI. Small thing, but it keeps you honest.&lt;/p&gt;

&lt;p&gt;As for third-party stuff, the usual suspects in &lt;code&gt;dev-dependencies&lt;/code&gt; are &lt;a href="https://crates.io/crates/pretty_assertions" rel="noopener noreferrer"&gt;&lt;code&gt;pretty_assertions&lt;/code&gt;&lt;/a&gt; (a diff instead of a wall of text when comparing big structs) and &lt;a href="https://crates.io/crates/proptest" rel="noopener noreferrer"&gt;&lt;code&gt;proptest&lt;/code&gt;&lt;/a&gt; - property-based testing, where you describe an invariant and the library generates hundreds of inputs and shrinks the counterexample for you. Some people prefer &lt;a href="https://crates.io/crates/quickcheck" rel="noopener noreferrer"&gt;&lt;code&gt;quickcheck&lt;/code&gt;&lt;/a&gt;, it's older and simpler. For snapshots there's &lt;a href="https://crates.io/crates/insta" rel="noopener noreferrer"&gt;&lt;code&gt;insta&lt;/code&gt;&lt;/a&gt;. Async tests get wrapped in &lt;code&gt;#[tokio::test]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One thing you don't get out of the box at all is parametrized tests. &lt;a href="https://crates.io/crates/rstest" rel="noopener noreferrer"&gt;&lt;code&gt;rstest&lt;/code&gt;&lt;/a&gt; fills that gap - one test runs against several sets of inputs via &lt;code&gt;#[case]&lt;/code&gt;, and you get pytest-style fixtures on top:&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;#[rstest]&lt;/span&gt;
&lt;span class="nd"&gt;#[case(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="nd"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nd"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="nd"&gt;#[case(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="nd"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="nd"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="nd"&gt;#[case(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="nd"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="nd"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;#[case]&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;#[case]&lt;/span&gt; &lt;span class="n"&gt;pct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;#[case]&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;)&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="nf"&gt;apply_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pct&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;expected&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;Every &lt;code&gt;#[case]&lt;/code&gt; becomes a separate test with its own name in the output. Without it you'd be either copy-pasting the function or writing a loop that dies with a useless message somewhere on the third iteration.&lt;/p&gt;

&lt;p&gt;All of this works great until your function calls &lt;code&gt;reqwest::get(...)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code that talks to the outside world
&lt;/h2&gt;

&lt;p&gt;A production service rarely lives in a vacuum: a call to a payment provider here, a request to a neighboring microservice there. Running tests over the real network means depending on someone else's staging and its rate limits. Such tests get flaky, flaky tests get ignored, you know how it ends.&lt;/p&gt;

&lt;p&gt;There are two approaches, and they're not interchangeable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Abstract it away and mock the trait
&lt;/h3&gt;

&lt;p&gt;Put the HTTP interaction behind a trait, swap in a fake for tests:&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;#[cfg_attr(test,&lt;/span&gt; &lt;span class="nd"&gt;mockall::automock)]&lt;/span&gt;
&lt;span class="k"&gt;trait&lt;/span&gt; &lt;span class="n"&gt;PaymentGateway&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;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Receipt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PayError&lt;/span&gt;&lt;span class="o"&gt;&amp;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;&lt;a href="https://crates.io/crates/mockall" rel="noopener noreferrer"&gt;&lt;code&gt;mockall&lt;/code&gt;&lt;/a&gt; generates a &lt;code&gt;MockPaymentGateway&lt;/code&gt; with configurable expectations: which method gets called, with what arguments and how many times. These tests are fast and depend on nothing external.&lt;/p&gt;

&lt;p&gt;The catch is that you're mocking your own abstraction, not the actual interaction. A trait mock will confirm you called &lt;code&gt;charge(100)&lt;/code&gt;. Whether a correct POST with the right &lt;code&gt;Content-Type&lt;/code&gt; actually went over the wire, or what happens when the server answers with a 503 - none of that is covered.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spin up a real server
&lt;/h3&gt;

&lt;p&gt;The second way is to run a local HTTP server right inside the test and have it pretend to be the external API. Client code doesn't change, all you need is a configurable base URL. The request goes through the whole stack for real, down to TCP.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://crates.io/crates/mockito" rel="noopener noreferrer"&gt;&lt;code&gt;mockito&lt;/code&gt;&lt;/a&gt; is the veteran. The shortest path from zero to a working 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;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;mockito&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Server&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="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;mock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="nf"&gt;.mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/hello"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.with_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.with_body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"world"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// ... hit server.url() with your client ...&lt;/span&gt;

&lt;span class="n"&gt;mock&lt;/span&gt;&lt;span class="nf"&gt;.assert&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://crates.io/crates/wiremock" rel="noopener noreferrer"&gt;&lt;code&gt;wiremock&lt;/code&gt;&lt;/a&gt; is async-first, inspired by the Java library of the same name. Composable matchers, fits naturally with &lt;code&gt;tokio&lt;/code&gt;:&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="nn"&gt;Mock&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;.and&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/orders"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;.respond_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ResponseTemplate&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="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.mount&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;mock_server&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;If your service runs on &lt;code&gt;axum&lt;/code&gt;, you'll probably pick this one and won't regret it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://crates.io/crates/httpmock" rel="noopener noreferrer"&gt;&lt;code&gt;httpmock&lt;/code&gt;&lt;/a&gt; does all of the above plus a standalone mode: the server can run separately and be shared across tests, or even across languages in a polyglot project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://crates.io/crates/whyhttp" rel="noopener noreferrer"&gt;&lt;code&gt;whyhttp&lt;/code&gt;&lt;/a&gt; is a recent one. It runs in a background thread with no async runtime, which is handy for sync tests, but the more interesting part is that it splits matchers into routing (&lt;code&gt;when&lt;/code&gt;) and validation (&lt;code&gt;should&lt;/code&gt;):&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;let&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;whyhttp&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Whyhttp&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;server&lt;/span&gt;
    &lt;span class="nf"&gt;.when&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/orders"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// which request we serve&lt;/span&gt;
    &lt;span class="nf"&gt;.should&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;r#"{"qty":1}"#&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;// what it must look like&lt;/span&gt;
    &lt;span class="nf"&gt;.response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's why the split matters. In most libraries the request body is part of the matching condition. Send &lt;code&gt;{"qty":2}&lt;/code&gt; instead of &lt;code&gt;{"qty":1}&lt;/code&gt; and the mock "isn't found", so the client gets a 404 and the test blows up somewhere in deserialization with a message that has nothing to do with the actual cause. With &lt;code&gt;whyhttp&lt;/code&gt; that request still gets its 201 and the scenario runs to the end, while the body mismatch shows up in the final report telling you exactly which check failed. The report is printed when the server is dropped, so forgetting to call &lt;code&gt;verify()&lt;/code&gt; and shipping an evergreen test just can't happen here.&lt;/p&gt;

&lt;p&gt;Conceptually all four work the same (set up expectations, swap the URL), so migrating between them is cheap.&lt;/p&gt;

&lt;h3&gt;
  
  
  A third way: run the real dependency in a container
&lt;/h3&gt;

&lt;p&gt;Sometimes you don't want to mock at all. A mock of S3 is always somebody's fantasy about how S3 behaves, and the subtle stuff like listing pagination or multipart uploads will be wrong in it. Same goes for databases, queues and really any sufficiently complex service.&lt;/p&gt;

&lt;p&gt;This is where the testcontainers approach has been gaining traction: the test starts a docker container with the real dependency, runs against it and kills the container afterwards. In Rust that's the &lt;a href="https://crates.io/crates/testcontainers" rel="noopener noreferrer"&gt;&lt;code&gt;testcontainers&lt;/code&gt;&lt;/a&gt; crate plus &lt;a href="https://crates.io/crates/testcontainers-modules" rel="noopener noreferrer"&gt;&lt;code&gt;testcontainers-modules&lt;/code&gt;&lt;/a&gt; with ready-made images:&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;testcontainers_modules&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="nn"&gt;minio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;MinIO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;testcontainers&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;runners&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AsyncRunner&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;uploads_report_to_s3&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;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;MinIO&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.start&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="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="nf"&gt;.get_host_port_ipv4&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9000&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="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;s3_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://localhost:{port}"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="c1"&gt;// from here on we're talking to a real S3-compatible store&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of an S3 mock you get a real MinIO. For a third-party API, grab its official docker image, or LocalStack if it's AWS. The price is speed: a container takes seconds to start, not milliseconds, and CI needs docker access. So containers don't replace HTTP mocks, they complement them: mocks for testing your client (those headers and retries), containers for when the behavior of the dependency itself matters.&lt;/p&gt;

&lt;p&gt;By the way, don't confuse this with VS Code devcontainers - those solve a different problem, reproducible dev environments. Though they help with testing too: the same image for the whole team and CI means "works on my machine" stops being an argument.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;Business logic gets plain unit tests plus property-based ones, no network, no mocks. The more logic you pull out into pure functions, the cheaper it is to cover.&lt;/p&gt;

&lt;p&gt;Integration with external APIs goes through a local mock server. That's where you check serialization, headers, reactions to error statuses, retries. For an HTTP client this is the most valuable layer: it catches exactly the bugs that trait mocks miss.&lt;/p&gt;

&lt;p&gt;For orchestration ("payment failed -&amp;gt; order marked as failed") trait mocks from &lt;code&gt;mockall&lt;/code&gt; are enough, bytes on the wire don't matter there. And where the dependency's behavior is more complex than you'd care to mock, testcontainers it is.&lt;/p&gt;

&lt;p&gt;Plus a couple of real e2e tests against actual staging, in a separate suite that doesn't block CI and whose flaky nature everyone has honestly agreed on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Testing HTTP code in Rust used to boil down to "hide everything behind traits and suffer". These days a mock server is one line away, and libraries compete on ergonomics rather than feature lists: how good the error reports are, whether you can even forget to verify.&lt;/p&gt;

&lt;p&gt;The compiler still won't check that the JSON you send is the right one. But the test for that now takes a minute to write.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>testing</category>
      <category>http</category>
      <category>mocking</category>
    </item>
  </channel>
</rss>
