<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Testing on</title><link>/tags/testing/</link><description>Recent content in Testing on</description><generator>Hugo</generator><language>en</language><lastBuildDate>Tue, 25 Mar 2025 00:00:00 +0000</lastBuildDate><atom:link href="/tags/testing/index.xml" rel="self" type="application/rss+xml"/><item><title>Lesson 10: Test Architecture — Tests that survive refactoring</title><link>/post/go/go-testing-architecture/</link><pubDate>Tue, 25 Mar 2025 00:00:00 +0000</pubDate><guid>/post/go/go-testing-architecture/</guid><description>&lt;p&gt;Every test suite starts clean. Then the codebase grows, the team grows, deadlines hit, and gradually the tests become the thing you dread touching. You add a feature and a hundred tests break — not because the feature is wrong, but because the tests were coupled to implementation details. You rename a method and spend more time updating tests than writing the feature. Sound familiar? The problem isn&amp;rsquo;t the tests themselves. It&amp;rsquo;s the architecture — the structure, the layering, and the principles that determine whether your test suite stays an asset or becomes a liability.&lt;/p&gt;</description></item><item><title>Lesson 9: Flaky Test Control — A flaky test is worse than no test</title><link>/post/go/go-testing-flaky/</link><pubDate>Sat, 15 Feb 2025 00:00:00 +0000</pubDate><guid>/post/go/go-testing-flaky/</guid><description>&lt;p&gt;A flaky test is a test that sometimes passes and sometimes fails with no code change in between. It sounds like a minor annoyance. It&amp;rsquo;s actually corrosive. Once your team learns that the test suite sometimes fails for &amp;ldquo;no reason,&amp;rdquo; they start merging on red. They start dismissing failures. They lose trust in the test suite as a signal. A single reliably-failing test is informative; a dozen flaky tests teach your team to ignore failures. I&amp;rsquo;ve seen this destroy test suite culture on multiple teams.&lt;/p&gt;</description></item><item><title>Lesson 8: Race-Aware Tests — If it passes without -race, it hasn't passed</title><link>/post/go/go-testing-race-aware/</link><pubDate>Mon, 20 Jan 2025 00:00:00 +0000</pubDate><guid>/post/go/go-testing-race-aware/</guid><description>&lt;p&gt;A test suite that passes without &lt;code&gt;-race&lt;/code&gt; is not a clean bill of health. It&amp;rsquo;s a test suite that hasn&amp;rsquo;t checked one of the most insidious categories of bugs in Go: data races. I&amp;rsquo;ve shipped code that passed every test, passed code review, and then caused memory corruption in production because two goroutines were reading and writing a map concurrently. The race detector would have found it in under a second. We just never ran it.&lt;/p&gt;</description></item><item><title>Lesson 7: Golden File Testing — Store expected output, compare on run</title><link>/post/go/go-testing-golden-files/</link><pubDate>Thu, 05 Dec 2024 00:00:00 +0000</pubDate><guid>/post/go/go-testing-golden-files/</guid><description>&lt;p&gt;Some functions produce output that&amp;rsquo;s too large or too structured to assert inline. A template renderer, a code generator, a JSON serializer for a deeply nested type, a CLI help text formatter — any of these can produce hundreds of lines of output that you need to verify is correct. Hardcoding that expected output inside the test function creates a wall of string literals. The golden file pattern solves this by storing the expected output in a file, comparing against it at test time, and offering a flag to regenerate it when the output intentionally changes.&lt;/p&gt;</description></item><item><title>Lesson 6: Mocking Alternatives — Interfaces over mocks, always</title><link>/post/go/go-testing-mocking/</link><pubDate>Tue, 05 Nov 2024 00:00:00 +0000</pubDate><guid>/post/go/go-testing-mocking/</guid><description>&lt;p&gt;Mocking has a bad reputation in the Go community, and some of it is deserved. Not because mocks are inherently wrong, but because the way most people use them — generating verbose stubs from interfaces, asserting on call counts, verifying argument order — produces tests that are coupled to implementation details rather than behaviour. Refactor the internals of a function without changing its public contract, and suddenly half your mocks break. That&amp;rsquo;s not a test problem. That&amp;rsquo;s a mock-as-test-double problem.&lt;/p&gt;</description></item><item><title>Lesson 5: Database Testing with Testcontainers — Mock the DB and you mock the truth</title><link>/post/go/go-testing-database/</link><pubDate>Thu, 10 Oct 2024 00:00:00 +0000</pubDate><guid>/post/go/go-testing-database/</guid><description>&lt;p&gt;I used to mock databases. I had a clean &lt;code&gt;Store&lt;/code&gt; interface, a &lt;code&gt;MockStore&lt;/code&gt; implementation for tests, and 100% coverage on my service layer. I felt good about it. Then we migrated from MySQL to PostgreSQL and discovered that a dozen subtle behaviours we&amp;rsquo;d been mocking around were wrong — &lt;code&gt;UPSERT&lt;/code&gt; semantics, NULL handling in &lt;code&gt;GROUP BY&lt;/code&gt;, timestamp precision, transaction isolation differences. The mock had been lying to us for months. Testcontainers fixed that.&lt;/p&gt;</description></item><item><title>Lesson 12: Test Architecture — When to unit, integration, or e2e</title><link>/post/rust/rust-test-architecture/</link><pubDate>Sun, 08 Sep 2024 11:10:00 +0000</pubDate><guid>/post/rust/rust-test-architecture/</guid><description>&lt;p&gt;I inherited a Rust project with 800 tests. Running them took 45 minutes. I dug in and found: 600 unit tests that mocked every dependency (most tested nothing meaningful), 180 integration tests that duplicated what the unit tests already covered, and 20 end-to-end tests that were flaky because they hit a staging server. The project had &lt;em&gt;more tests&lt;/em&gt; than any codebase I&amp;rsquo;d seen — and also &lt;em&gt;more bugs&lt;/em&gt;. The volume was high, but the strategy was garbage.&lt;/p&gt;</description></item><item><title>Lesson 11: Testing in CI — GitHub Actions for Rust</title><link>/post/rust/rust-test-ci/</link><pubDate>Thu, 05 Sep 2024 16:45:00 +0000</pubDate><guid>/post/rust/rust-test-ci/</guid><description>&lt;p&gt;I merged a PR last year that passed all tests on my M2 MacBook and broke on the Linux CI runner. The issue? I&amp;rsquo;d used &lt;code&gt;std::path::PathBuf&lt;/code&gt; with hardcoded forward slashes, which worked fine on macOS but blew up on Linux because the test setup expected a specific path format. CI exists to catch exactly this kind of &amp;ldquo;it works on my machine&amp;rdquo; problem. Here&amp;rsquo;s how to set it up properly for Rust.&lt;/p&gt;</description></item><item><title>Lesson 10: Benchmarking with criterion — Measure, don't guess</title><link>/post/rust/rust-test-benchmarks/</link><pubDate>Mon, 02 Sep 2024 13:20:00 +0000</pubDate><guid>/post/rust/rust-test-benchmarks/</guid><description>&lt;p&gt;I once optimized a hot loop by replacing a &lt;code&gt;HashMap&lt;/code&gt; lookup with a &lt;code&gt;Vec&lt;/code&gt; indexed by a precomputed key. Felt clever. Ran the benchmarks. The &amp;ldquo;optimized&amp;rdquo; version was 15% &lt;em&gt;slower&lt;/em&gt; because the &lt;code&gt;Vec&lt;/code&gt; was huge, cache-cold, and the access pattern was random. Without the benchmark, I would&amp;rsquo;ve shipped that &amp;ldquo;improvement&amp;rdquo; and bragged about it. Performance intuition is unreliable. Measurement isn&amp;rsquo;t.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Rust is fast by default, but that doesn&amp;rsquo;t mean your code is fast. You can still write O(n^2) algorithms, cause cache thrashing, allocate unnecessarily, or clone data you could borrow. Worse, optimizations that seem obvious can backfire in ways you don&amp;rsquo;t expect.&lt;/p&gt;</description></item><item><title>Lesson 4: HTTP Handler Testing — httptest is your best friend</title><link>/post/go/go-testing-http-handlers/</link><pubDate>Sun, 01 Sep 2024 00:00:00 +0000</pubDate><guid>/post/go/go-testing-http-handlers/</guid><description>&lt;p&gt;Every Go HTTP handler is just a function that takes a &lt;code&gt;ResponseWriter&lt;/code&gt; and a &lt;code&gt;*Request&lt;/code&gt;. That&amp;rsquo;s the entire contract. The fact that the standard library ships with a testing package that lets you call that function with a fake writer and a real request — without starting a server, without binding a port — is a gift that too many people overlook. I spent months spinning up actual servers in tests before I discovered &lt;code&gt;net/http/httptest&lt;/code&gt;. I will not let you make the same mistake.&lt;/p&gt;</description></item><item><title>Lesson 9: Code Coverage — tarpaulin and llvm-cov</title><link>/post/rust/rust-test-coverage/</link><pubDate>Fri, 30 Aug 2024 07:55:00 +0000</pubDate><guid>/post/rust/rust-test-coverage/</guid><description>&lt;p&gt;I worked on a team that had a strict &amp;ldquo;90% code coverage&amp;rdquo; policy. Every PR had to hit that number or it got rejected. The result? People wrote tests like &lt;code&gt;assert!(true)&lt;/code&gt; just to touch lines. We had 92% coverage and bugs everywhere. Coverage is a useful signal. It&amp;rsquo;s a terrible goal.&lt;/p&gt;
&lt;p&gt;That said, knowing &lt;em&gt;which&lt;/em&gt; lines your tests don&amp;rsquo;t touch is genuinely valuable. It tells you where your blind spots are. Here&amp;rsquo;s how to measure it in Rust.&lt;/p&gt;</description></item><item><title>Lesson 8: Snapshot Testing with insta — Catch regressions instantly</title><link>/post/rust/rust-test-snapshot/</link><pubDate>Tue, 27 Aug 2024 10:30:00 +0000</pubDate><guid>/post/rust/rust-test-snapshot/</guid><description>&lt;p&gt;I refactored a code formatter once — changed how it handles indentation for nested blocks. I had assertions checking specific outputs for five test cases, and they all passed. But the formatter produced subtly different output for a sixth pattern I hadn&amp;rsquo;t tested, and a user filed a bug three days later. If I&amp;rsquo;d had snapshot tests, I would&amp;rsquo;ve seen every output change in a diff during the refactor. Five minutes of review instead of three days of embarrassment.&lt;/p&gt;</description></item><item><title>Lesson 7: Fuzz Testing with cargo-fuzz — Breaking your code automatically</title><link>/post/rust/rust-test-fuzzing/</link><pubDate>Sat, 24 Aug 2024 20:05:00 +0000</pubDate><guid>/post/rust/rust-test-fuzzing/</guid><description>&lt;p&gt;A friend of mine maintains a binary parser in Rust. It had hundreds of unit tests, property tests, the works. He ran a fuzzer against it on a Friday afternoon, left it running over the weekend. Monday morning: 23 unique crashes. Some were panics from unexpected inputs. Two were infinite loops. One was a stack overflow from deeply nested structures. All from inputs no human would ever think to construct.&lt;/p&gt;</description></item><item><title>Lesson 6: Property-Based Testing with proptest — Let the computer find your bugs</title><link>/post/rust/rust-test-property/</link><pubDate>Thu, 22 Aug 2024 15:40:00 +0000</pubDate><guid>/post/rust/rust-test-property/</guid><description>&lt;p&gt;I wrote a URL parser once. Had fifty hand-crafted test cases. All green. Pushed to production. Within a week, a user sent a URL with a percent-encoded space followed by a Unicode character, and the parser crashed. I never would have thought to write that test case. A property-based test would have found it in under a second.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;When you write tests by hand, you test the cases you think of. But bugs don&amp;rsquo;t live in the cases you think of — they live in the ones you don&amp;rsquo;t. Your test for &lt;code&gt;sort([3, 1, 2])&lt;/code&gt; passes, but does your sort handle a list with ten million duplicates? A list of all negative numbers? An empty list? A list with &lt;code&gt;i32::MAX&lt;/code&gt; and &lt;code&gt;i32::MIN&lt;/code&gt; adjacent?&lt;/p&gt;</description></item><item><title>Lesson 5: Mocking — mockall and faking dependencies</title><link>/post/rust/rust-test-mocking/</link><pubDate>Mon, 19 Aug 2024 08:15:00 +0000</pubDate><guid>/post/rust/rust-test-mocking/</guid><description>&lt;p&gt;I spent an embarrassing amount of time early in my Rust journey trying to mock a database connection by hand. I built a fake struct, implemented the trait, tracked method calls with &lt;code&gt;RefCell&amp;lt;Vec&amp;lt;...&amp;gt;&amp;gt;&lt;/code&gt; wrappers, wrote expectation-checking logic&amp;hellip; and ended up with 200 lines of mock code to test 15 lines of business logic. Then I found &lt;code&gt;mockall&lt;/code&gt; and felt like an idiot.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Real code has dependencies. Your payment processor calls Stripe. Your user service queries a database. Your notification system sends emails. You can&amp;rsquo;t — and shouldn&amp;rsquo;t — hit these real services in unit tests. They&amp;rsquo;re slow, flaky, expensive, and non-deterministic.&lt;/p&gt;</description></item><item><title>Lesson 4: Test Fixtures and Setup/Teardown — Reusable test infrastructure</title><link>/post/rust/rust-test-fixtures/</link><pubDate>Sat, 17 Aug 2024 11:30:00 +0000</pubDate><guid>/post/rust/rust-test-fixtures/</guid><description>&lt;p&gt;I had a test file last year where every single test started with the same twelve lines of setup — creating a database connection, inserting seed data, configuring a logger. Twelve lines, copy-pasted forty times. When I needed to change the seed data, I had to update forty tests. I missed three. Those three tests silently tested against stale data for two months.&lt;/p&gt;
&lt;p&gt;Fixtures exist to prevent this kind of insanity.&lt;/p&gt;</description></item><item><title>Lesson 3: Doc Tests — Tested documentation</title><link>/post/rust/rust-test-doc-tests/</link><pubDate>Wed, 14 Aug 2024 18:10:00 +0000</pubDate><guid>/post/rust/rust-test-doc-tests/</guid><description>&lt;p&gt;I once read the docs for a popular Rust crate, copied the example verbatim, and it didn&amp;rsquo;t compile. The API had changed two versions ago but nobody updated the docs. I spent twenty minutes debugging code that was supposed to be the &amp;ldquo;getting started&amp;rdquo; guide. Rust has a built-in solution to this exact problem, and it&amp;rsquo;s one of the most underappreciated features in the language.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Documentation lies. Not on purpose — it lies because code evolves and docs don&amp;rsquo;t. A function signature changes, a parameter gets renamed, a return type shifts from &lt;code&gt;String&lt;/code&gt; to &lt;code&gt;&amp;amp;str&lt;/code&gt;, and the example in the docstring quietly becomes fiction. No compiler warning, no test failure, just a frustrated user copy-pasting broken code.&lt;/p&gt;</description></item><item><title>Lesson 2: Integration Tests — The tests/ directory</title><link>/post/rust/rust-test-integration/</link><pubDate>Mon, 12 Aug 2024 09:45:00 +0000</pubDate><guid>/post/rust/rust-test-integration/</guid><description>&lt;p&gt;A colleague once reviewed one of my Rust libraries and said, &amp;ldquo;Your unit tests pass, but I can&amp;rsquo;t actually use the crate — the public API doesn&amp;rsquo;t compose the way anyone would expect.&amp;rdquo; He was right. I&amp;rsquo;d tested every internal function meticulously and completely ignored the experience of someone actually calling my library from the outside. That&amp;rsquo;s the gap integration tests fill.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Unit tests live inside your source files and can access private functions. That&amp;rsquo;s great for verifying internal logic, but it creates a blind spot: you never validate that your &lt;em&gt;public API&lt;/em&gt; actually makes sense. You can have perfect internal functions that combine into a terrible user experience.&lt;/p&gt;</description></item><item><title>Lesson 1: Unit Tests — #[test] and assertions</title><link>/post/rust/rust-test-unit-basics/</link><pubDate>Sat, 10 Aug 2024 14:22:00 +0000</pubDate><guid>/post/rust/rust-test-unit-basics/</guid><description>&lt;p&gt;I shipped a Rust library last year that had zero tests. Not because I&amp;rsquo;m lazy — I was prototyping, moving fast, &amp;ldquo;I&amp;rsquo;ll add tests later.&amp;rdquo; You know how that story ends. A one-character typo in a boundary check sat in production for three weeks before someone&amp;rsquo;s data got silently corrupted. Three weeks. I&amp;rsquo;d have caught it with one &lt;code&gt;assert_eq!&lt;/code&gt; and thirty seconds of effort.&lt;/p&gt;
&lt;p&gt;Never again. Here&amp;rsquo;s how testing actually works in Rust, from the ground up.&lt;/p&gt;</description></item><item><title>Lesson 3: Integration Tests — Unit tests lie, integration tests prove</title><link>/post/go/go-testing-integration/</link><pubDate>Thu, 01 Aug 2024 00:00:00 +0000</pubDate><guid>/post/go/go-testing-integration/</guid><description>&lt;p&gt;Unit tests feel productive. You write a function, you write a test, everything goes green, and you merge. But unit tests exist in a bubble — a bubble where every dependency returns exactly what you told it to return. The real world doesn&amp;rsquo;t do that. Your database serializes a &lt;code&gt;time.Time&lt;/code&gt; differently than you expect. Your HTTP client follows a redirect your mock never mentioned. Your message queue drops a message under backpressure that your fake queue cheerfully delivered. I&amp;rsquo;ve had unit test suites where every test passed and the feature didn&amp;rsquo;t work in staging. Integration tests are what close that gap.&lt;/p&gt;</description></item><item><title>Lesson 2: Fuzzing in Go — Let the machine find your edge cases</title><link>/post/go/go-testing-fuzzing/</link><pubDate>Wed, 10 Jul 2024 00:00:00 +0000</pubDate><guid>/post/go/go-testing-fuzzing/</guid><description>&lt;p&gt;There&amp;rsquo;s a class of bugs that you will never find by thinking about edge cases. You&amp;rsquo;ll think about empty strings, about zero values, about negative numbers. But will you think about the string that&amp;rsquo;s exactly 65,536 bytes? Or the UTF-8 sequence that&amp;rsquo;s technically valid but trips up a specific parser codepath? Or the floating point value that serializes and then fails to deserialize because of a precision edge in your JSON handling? You won&amp;rsquo;t. But a fuzzer will find it in under a minute.&lt;/p&gt;</description></item><item><title>Lesson 1: Subtests and t.Run — Name your test cases or debug blind</title><link>/post/go/go-testing-subtests/</link><pubDate>Sat, 15 Jun 2024 00:00:00 +0000</pubDate><guid>/post/go/go-testing-subtests/</guid><description>&lt;p&gt;I used to write tests the way I wrote the first functions I ever wrote in Go — flat, repetitive, and with names so generic that when they failed, I had no idea which case broke. &amp;ldquo;TestParseDate failed&amp;rdquo; is not information. It&amp;rsquo;s a dare. Go figure out which of the twelve implicit cases in the body is responsible. I wasted hours doing exactly that before I committed to subtests.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;t.Run&lt;/code&gt; is one of those language features that looks like a small convenience and turns out to be load-bearing for any serious test suite. Once you start using it, you wonder how you ever shipped anything without it.&lt;/p&gt;</description></item></channel></rss>