diff --git a/content/blog/tdd-overkill-myth-lightweight-ruby/cover.png b/content/blog/tdd-overkill-myth-lightweight-ruby/cover.png new file mode 100644 index 000000000..98ef0800b Binary files /dev/null and b/content/blog/tdd-overkill-myth-lightweight-ruby/cover.png differ diff --git a/content/blog/tdd-overkill-myth-lightweight-ruby/index.md b/content/blog/tdd-overkill-myth-lightweight-ruby/index.md new file mode 100644 index 000000000..d9b463bfc --- /dev/null +++ b/content/blog/tdd-overkill-myth-lightweight-ruby/index.md @@ -0,0 +1,107 @@ +--- +title: 'TDD Without the Overkill: Why Lightweight TDD Ships Faster' +description: 'TDD looks slow on paper. The real ledger is one hour writing tests against six hours debugging next week. Here is why lightweight TDD wins.' +date: 2026-05-04 +draft: false +author: 'JetThoughts Team' +slug: tdd-overkill-myth-lightweight-ruby +keywords: 'tdd overkill myth, lightweight tdd, tdd vs no tests time, tdd design not coverage' +tags: ['tdd', 'ruby', 'productivity', 'engineering', 'startup'] +categories: ['Engineering'] +cover_image: cover.png +metatags: + image: cover.png + og_title: 'TDD Without the Overkill: Why Lightweight TDD Ships Faster' + og_description: 'TDD looks slow on paper. The real ledger is one hour writing tests against six hours debugging next week. Here is why lightweight TDD wins.' +cover_image_alt: 'Obsidian-dark cover with the headline TDD Without the Overkill, a faceted ruby gem on the right, and three chips reading LEDGER 1h vs 6h, LOOP 90 seconds, OUTPUT design' +canonical_url: 'https://jetthoughts.com/blog/tdd-overkill-myth-lightweight-ruby/' +related_posts: false +--- + +The senior dev who told you TDD was overkill learned it on a project where the test suite took twenty minutes to run and every PR took two rounds of mock refactoring. So did we. What Kent Beck described in 2003 and what most teams call "TDD" in 2026 are not the same practice. Most of the loudest "TDD is too slow" complaints describe a workflow that has nothing to do with the cycle Beck laid out. + +If you want the rhythm worked out on real Ruby code, we walk through four cycles on a small `Order` class in [TDD in Ruby: A Step-by-Step Guide](/blog/test-driven-development-tdd-in-ruby-step-by-guide-tutorial-bestpractices/). + +## The "TDD is overkill" myth comes from heavyweight habits + +The Agile Institute frames the [time ledger of TDD](https://agileinstitute.com/articles/dispelling-myths-about-tdd) plainly. One hour of writing code without tests usually buys you six hours of debugging the following week. One hour of writing code with TDD usually cuts that debug bill to a fraction. Either way you spend six hours. Only the TDD path leaves you with a design you can change. + +Thirty-minute red-green cycles are the first culprit. Beck's original cycle measures in seconds and minutes. A team that writes one giant test, builds an entire feature, then fights ten unrelated mock failures has done integration testing after the fact, not TDD. On a billing-platform rescue last quarter the average cycle ran thirty-eight minutes because every spec booted Rails. The team kept TDD and learned to write unit tests that ran in 8ms instead. + +Mocks are the second trap. A new developer reads "test in isolation," wraps every collaborator in a stub, and ends up with a test suite that asserts the structure of the code instead of its behavior. Change a method name, twenty tests fail. None of them caught a real bug. We covered the specific shape of this failure mode in [Mock Everything: A Good Way to Sink TDD Testing](/blog/mock-everything-good-way-sink-tdd-testing/). + +Then there's scenario sprawl. ATDD, BDD, and Cucumber all promised non-technical stakeholders could write executable specs. In practice, your PM never wrote a single Gherkin scenario, and your engineers maintained a 4,000-line `features/` directory nobody trusted while the unit tests did the actual work. + +## What lightweight TDD actually feels like + +The cycle is short. Write one assertion that fails for the right reason. Type the smallest method body that makes it green - a hardcoded return value is fine, that is what Sandi Metz means by [Shameless Green](https://sandimetz.com/99bottles): code cheap enough to throw away when the next test forces a better shape. Commit. Refactor only if there is a real duplication to remove, then commit again, separately. + +The first cycle on a fresh `Order` class is six lines of code: + +```ruby +# test +assert_equal 0, Order.new.total + +# implementation +class Order + def total = 0 +end +``` + +That's not a typo. The simplest method body that turns the test green is a hardcoded zero. Two more cycles - adding an item, summing two items - replace the hardcode with the actual logic. The full progression lives in the step-by-step guide. + +Ninety seconds per cycle, not thirty minutes. Each cycle ends with a green test and a commit, so reverting to the last green state costs ninety seconds of work, not an afternoon. Heavyweight TDD lost this on purpose. Once cycles drift past 30 minutes, the safety net stops paying for itself. + +We walk through three more cycles on the same `Order` class in the [step-by-step guide](/blog/test-driven-development-tdd-in-ruby-step-by-guide-tutorial-bestpractices/), with timestamps showing how each lands in under two minutes. + +## The actual prize is design, not coverage + +The biggest mistake new TDD readers make is treating coverage as the goal. It is not. When a test is hard to write, the design is what's wrong. Setup that runs ten lines means your object has too many dependencies. A method that needs Rails booted to run wants to live somewhere else. Behaviour changes that cascade into eight test rewrites tell you the seams are in the wrong place. J.B. Rainsberger calls this [listening to the cries of the test](https://blog.thecodewhisperer.com/permalink/the-myth-of-advanced-tdd). Lightweight TDD turns that pain into a refactoring trigger instead of a reason to abandon the discipline. + +The payoff is refactor courage. With a green suite under you, renaming a class is a thirty-second move. Without it, the rename becomes a four-hour archaeology project where you read every caller and pray. The last three Rails rescues we picked up all had tests in the 2-5% range, and every refactor proposal turned into "let's not touch it." + +Design exploration follows. The test forces you to write the call site before the implementation, so if the call site reads badly, you learn that in 90 seconds, before any production code commits to the bad shape. + +Then there's fearless deletion. Code with tests around it can be removed with confidence. That 600-line service object nobody calls anymore? Run the suite, delete it, run the suite again. Without tests, dead code rots in the repo for years because the cost of being wrong about whether anything still uses it is too high. + +Teams that chase coverage as the goal end up with suites full of `assert_not_nil(@order)` assertions that never test the actual behaviour. + +## When TDD really is overkill + +A 40-line Rake task that backfills one column on one production table - the one you will run twice and delete next week - does not need a test. Run it on staging, eyeball the output, run it on production, delete the file. The same logic covers most one-off data migrations: the script is a transaction that runs once, and writing a test against a populated dev database often costs more than running the migration twice. + +Prototype spikes and hackathon demos earn the same exemption. The point of the spike is to learn whether an idea is worth building - write the messy code, get the answer, throw it away. With eighteen hours on the clock and judges who will not open the repo, optimise for the demo running. Whoever picks up the project Monday morning can deal with the design - and they usually won't have to, because most hackathon demos never ship. If the spike survives, you will rewrite it with tests on the way to production, and that rewrite is when the discipline starts paying. + +The rule of thumb that catches all four cases: will this code be touched again? If the answer is no, skip TDD and feel no guilt. If the answer is yes, even "yes, in three months," the ledger flips and you should write the test. The Rails app you are still maintaining six months later is the one where the missing tests cost you four hours every time you change something. + +## What changes when your team adopts the rhythm + +Code review gets shorter. Separate structural commits (rename, extract) from behavioural ones (new branch, new result), and reviewers scan the structural moves in seconds while focusing on actual behaviour changes. Beck made this separation the spine of [Tidy First?](https://tidyfirst.substack.com/) (2023). Look at the difference in commit shape. + +```ruby +# bundled commit (3 files changed, 47 lines) +# - extracted PriceCalculator +# - added bulk discount branch +# - renamed total -> total_with_tax + +# separated commits +# (1) Tidy: extract PriceCalculator (refactor, no behaviour change) +# (2) Tidy: rename total (refactor, no behaviour change) +# (3) Feat: bulk discount branch (behaviour change, with new test) +``` + +The bundled commit needs a careful reviewer for an hour. The three separated commits review in twelve minutes total because (1) and (2) only need to confirm the suite stayed green. Tidy First commits are cheap to review precisely because TDD made them safe to make. + +Regressions surface during the cycle that introduces them. The red bar interrupts the developer while the change is still in their head; production logs interrupt them three weeks later, mid-context-switch on something else. Debugging cost drops as a side effect, because every micro-commit is a known-good state. A CI failure two commits later costs you a two-minute `git bisect` and a ninety-second revert, not a four-hour archaeology session. + +A 200-line setup block is your object asking to be split. When a test needs eight mocks to run, the collaborator graph wants flattening. The Ruby and Rails patterns we see most often when this happens are catalogued in [Test-Driven Thinking for Solving Common Ruby Pitfalls](/blog/test-driven-thinking-for-solving-common-ruby-pitfalls-rails-tdd/). That discipline is exactly what's missing in the codebases founders hand us when an MVP starts breaking. + +## What a TDD rescue actually starts with + +We have done forty-plus Rails rescues over seventeen years. The pattern that brings founders to our door is consistent: a vibe-coded MVP, no automated tests (the suites we inherit average under 5% line coverage), no CI worth running, and a founder who has spent somewhere between $80K and $200K. The first user found a critical bug in week two of beta. + +The fix never starts with adding test coverage to the existing code. That is the trap. We start by writing a single failing test that captures the bug the founder is currently bleeding from, fixing it inside the 90-second loop, and shipping the patch the same day. Then we write the next failing test for the next bleeding bug. We covered the rationale on this approach in [Why and How to Use TDD: Tips for Testing](/blog/why-how-use-tdd-main-tips-testing/). + +If you wrote the MVP yourself and the bugs are showing up faster than you can ship features - or you are the senior dev being asked to rescue someone else's vibe-coded build - that is the situation we audit for free. One senior developer reads your codebase and writes a one-page assessment naming the three changes that pay back fastest. No contract, no follow-up sales call. Send the repo URL and a paragraph on what's breaking to [/contact-us/](/contact-us/) and we respond inside 48 hours. + +Writing the test is the cheapest moment in a piece of code's life. The rename three weeks from now will cost more. So will the refactor you keep putting off because the suite is too thin to catch what breaks. diff --git a/content/blog/test-driven-development-tdd-in-ruby-step-by-guide-tutorial-bestpractices/cover.jpeg b/content/blog/test-driven-development-tdd-in-ruby-step-by-guide-tutorial-bestpractices/cover.jpeg deleted file mode 100644 index fe5a01b90..000000000 Binary files a/content/blog/test-driven-development-tdd-in-ruby-step-by-guide-tutorial-bestpractices/cover.jpeg and /dev/null differ diff --git a/content/blog/test-driven-development-tdd-in-ruby-step-by-guide-tutorial-bestpractices/cover.png b/content/blog/test-driven-development-tdd-in-ruby-step-by-guide-tutorial-bestpractices/cover.png new file mode 100644 index 000000000..563a2f2c6 Binary files /dev/null and b/content/blog/test-driven-development-tdd-in-ruby-step-by-guide-tutorial-bestpractices/cover.png differ diff --git a/content/blog/test-driven-development-tdd-in-ruby-step-by-guide-tutorial-bestpractices/index.md b/content/blog/test-driven-development-tdd-in-ruby-step-by-guide-tutorial-bestpractices/index.md index 56491e758..5ed876047 100644 --- a/content/blog/test-driven-development-tdd-in-ruby-step-by-guide-tutorial-bestpractices/index.md +++ b/content/blog/test-driven-development-tdd-in-ruby-step-by-guide-tutorial-bestpractices/index.md @@ -1,26 +1,215 @@ --- -remote_url: https://dev.to/jetthoughts/test-driven-development-tdd-in-ruby-a-step-by-step-guide-2h15 -source: dev_to -remote_id: 2066540 -dev_to_id: 2066540 -dev_to_url: https://dev.to/jetthoughts/test-driven-development-tdd-in-ruby-a-step-by-step-guide-2h15 -title: 'Test-Driven Development (TDD) in Ruby: A Step-by-Step Guide' -description: In Test-Driven Development (TDD), you start with tests, not code. First, write a test that defines... -created_at: '2024-10-30T10:03:39Z' -edited_at: '2024-11-25T15:42:01Z' -date: 2024-10-30 +title: 'TDD in Ruby: A Step-by-Step Guide' +description: A working Ruby tutorial for TDD on a small Order class. The 90-second loop, Shameless Green, Tidy First commits, and the reset move that makes it all safe. +created_at: '2026-05-04T10:00:00Z' +edited_at: '2026-05-04T10:00:00Z' +date: 2026-05-04 draft: false tags: -- tutorial -- bestpractices - ruby +- bestpractices +- tutorial - learning +- tdd +- testing canonical_url: https://jetthoughts.com/blog/test-driven-development-tdd-in-ruby-step-by-guide-tutorial-bestpractices/ -cover_image: https://raw.githubusercontent.com/jetthoughts/jetthoughts.github.io/master/content/blog/test-driven-development-tdd-in-ruby-step-by-guide-tutorial-bestpractices/cover.jpeg +cover_image: cover.png +cover_image_alt: 'Obsidian-dark cover with the headline TDD in Ruby, a faceted ruby gem on the right, and three chips reading LOOP 90 seconds, COMMITS micro, GREEN shameless' metatags: - image: cover.jpeg + image: cover.png slug: test-driven-development-tdd-in-ruby-step-by-guide-tutorial-bestpractices --- -In Test-Driven Development (TDD), you start with tests, not code. First, write a test that defines what the code should do. Then write the code to make the test pass, focusing on small, clear steps. This approach in Ruby helps keep each part of the code clear and on target with what it’s supposed to do, often making it easier to adjust and maintain down the line. TDD can make a difference in breaking down problems into smaller, manageable steps, which tends to reduce bugs and make sure each part does what’s actually needed. -In this article, we’ll explore the TDD lifecycle—red, green, refactor—through a small example of an `Order` class that handles items and calculates the total price. +You opened this post because you want to try TDD on real Ruby code, and the tutorials you've seen so far either skipped the rhythm entirely or buried it under three pages of theory. This one shows the loop on a small `Order` class you can paste into a Minitest file and follow along with. + +We'll write four cycles in under eight minutes. Each cycle ends with a green test and a commit, so at any point you can `git reset --hard` and you've lost at most 90 seconds of work. That bounded cost is what makes the whole discipline tractable - and it's the part most TDD intros leave out. + +## Why TDD looks slow but isn't + +The Agile Institute frames the time ledger plainly: a typical hour of code without tests usually buys you six hours of debugging the following week, while an hour with TDD usually buys close to zero. The total clock time is similar; the difference is what's in your repo at the end. One version ships with bugs you haven't found yet. The other ships with a design you can change. + +A separate complaint - "TDD is overkill for what I'm doing" - is a different argument, and we'll dig into the overkill myth in a follow-up post. For now, assume you're willing to give the discipline a fair try; this post focuses on the rhythm itself. + +## The 90-second loop on a small Order class + +The cycle is RED, then SHAMELESS GREEN, then COMMIT, then REFACTOR if anything's worth tidying, then COMMIT again. We'll do four cycles, with timestamps so you can feel the pace. + +Start with one test file. Minitest because it's what ships in stdlib and because the project this post lives in uses it. + +```ruby +# 00:00 RED - cycle 1 +require "minitest/autorun" + +class OrderTest < Minitest::Test + def test_empty_order_total_is_zero + assert_equal 0, Order.new.total + end +end +``` + +Run it. `NameError: uninitialized constant Order`. That counts as red - the test fails for the right reason. + +```ruby +# 00:30 SHAMELESS GREEN - cycle 1 +class Order + def total + 0 + end +end +``` + +Tests pass. `git commit -m "Order responds to #total"`. 90 seconds in, one green test, one commit, and an `Order` that returns zero. We'll fix the lie shortly. + +```ruby +# 02:00 RED - cycle 2 +def test_total_sums_a_single_item + order = Order.new + order.add(price: 1000) + assert_equal 1000, order.total +end +``` + +Red - `NoMethodError: undefined method 'add'`. + +```ruby +# 02:45 SHAMELESS GREEN - cycle 2 +class Order + def initialize + @items = [] + end + + def add(price:) + @items << price + end + + def total + @items.sum + end +end +``` + +Green. Both tests pass - `[].sum` is zero, so the empty-order test still works. Commit. + +```ruby +# 04:30 RED - cycle 3 +def test_total_sums_multiple_items + order = Order.new + order.add(price: 1000) + order.add(price: 2500) + assert_equal 3500, order.total +end +``` + +Run it. It passes on first try. The cycle 2 implementation already covered the multi-item case - we didn't need new production code, only a test that pinned the behavior down. Commit anyway: the test is part of the safety net now, and the commit log shows what we believed was true at this point. + +```ruby +# 06:00 RED - cycle 4 +def test_supports_quantity + order = Order.new + order.add(price: 1000, quantity: 3) + assert_equal 3000, order.total +end +``` + +Red - the existing `add` ignores the new keyword. + +```ruby +# 07:00 SHAMELESS GREEN - cycle 4 +def add(price:, quantity: 1) + @items << price * quantity +end +``` + +Green. Commit. Seven minutes in, four green tests, four commits, an `Order` that handles items, totals, and quantities. The whole class fits on one screen. + +The pace is the point. Each loop ends with green tests and a commit, so the worst possible state of your working tree is 90 seconds away from a known-good one. Compare that to writing 200 lines, running it, getting a stack trace, and spending 40 minutes finding the line that broke it. + +## Shameless Green - write the dumbest code that works + +Cycle 1's `def total; 0; end` looks embarrassing, and that's exactly the point. Sandi Metz, in [99 Bottles of OOP](https://sandimetz.com/99bottles), calls this Shameless Green: code that's "cheap to write, cheap to understand, cheap to change." It isn't trying to be clever. It's optimizing for the things you can actually verify in the moment (did this take me five minutes and does the next person who reads it understand it), and trading away the things you can't, like whether your abstraction will still feel right six months from now. + +The temptation, especially for senior Ruby developers, is to design the right shape now. You can already see the `LineItem` value object, the `OrderCalculator` service, the `PricingPolicy` strategy. Resist. After cycle 1 you have one example. After cycle 2 you have two. The [rule of three for abstraction](https://wiki.c2.com/?ThreeStrikesAndYouRefactor) exists because two examples lie about what they have in common - they always look more similar than they really are. + +KISS and YAGNI fold in here. KISS picks the simplest path that makes the test pass. YAGNI tells you not to add the configurable `currency:` parameter until a test demands it. The hardcoded `0` in cycle 1 satisfies both. By cycle 4 we have four concrete examples and the right shape - a sum over priced items with optional quantity - emerged on its own. We didn't design it; the tests did. + +JT's internal flocking-rules standard formalizes the move once you do have enough examples: select the things most alike, find the smallest difference between them, make the simplest change that removes the difference. Abstractions show up through convergence, not by guessing. The most common way Ruby developers wreck a TDD suite is by [reaching for mocks the moment a test gets uncomfortable](/blog/mock-everything-good-way-sink-tdd-testing/) - same root cause: trying to design ahead of the evidence the tests are giving you. + +## Tidy First - never bundle structure with behavior + +Look at cycle 4 again. Suppose along with adding quantity support you also rename `@items` to `@line_items` because it reads better. Now the commit contains two unrelated things: a behavior change (quantity arithmetic) and a structural change (rename). The reviewer can't tell which line caused the test to flip green. Six months from now, when someone runs `git blame` on the bug your quantity logic introduced, they land on a commit that did two things and the message only explained one. + +Kent Beck's 2023 book [Tidy First?](https://tidyfirst.substack.com/) makes the rule blunt: structural changes and behavioral changes go in separate commits. Beck calls structural changes "tidyings" - rename, extract, inline, reformat. They never change what the program does. Behavior changes do. Mixing them costs you twice: once during review, once during `git bisect`. + +Bad commit: + +``` +feat: add quantity support and rename @items to @line_items +``` + +Good commits, in order: + +``` +refactor: rename @items to @line_items +feat: add quantity support to Order#add +``` + +The cost of splitting is one extra commit. The benefits compound: `git revert` on the feature commit doesn't undo the rename, [code review reads top to bottom without context-switching](/blog/effortless-code-conventions-review-for-pull-request-changes-ruby-ci/), and the next developer can bisect a regression to the actual cause. The last 4,000-commit Rails repo we inherited had structural and behavioral changes mixed in roughly half of its commits, and rebuilding the discipline alone saved us days on the first three debugging sessions. + +## Auto-revert when red - `git reset --hard` is your friend + +Tests went red and you're not sure why? `git reset --hard HEAD`. You've lost at most 90 seconds of work, because that's how long it's been since the last green commit. Try again, smaller step. + +This is the move that makes Shameless Green safe. We commit on every green - cycle 1's `def total; 0; end` was committable code - so revert costs nothing. Junior Ruby developers find the move terrifying for about two weeks, then they find it liberating. You stop being precious about keystrokes you've already typed, because they cost you nothing to retype. The 90-second loop puts a hard ceiling on lost work, and that ceiling is what makes experimenting cheap enough to do twenty times in an afternoon. + +The 5-to-20-commits-per-hour rhythm Beck describes in *Tidy First?* isn't aspirational. It's how you actually work once you trust the safety net. On the last fintech rescue we ran (a Q1 2026 engagement, Rails 7.1, ~11k tests), the previous team committed once a day in 600-line bombs. Their `git bisect` on a regression was useless because every commit straddled the line between behavior change and refactor. We rebuilt the commit cadence first and debugged second. + +## Common newbie mistakes + +Three failure modes show up over and over when developers first try TDD on Ruby. Each one passes for the discipline at a glance, and each one quietly removes the thing that makes TDD pay off. + +The first is testing implementation instead of behavior. A junior developer writing the cycle 2 test reaches for the internal `@items` array because it feels concrete: + +```ruby +# Mistake: tests internals +def test_add_pushes_to_items_array + order = Order.new + order.add(price: 1000) + assert_equal [1000], order.instance_variable_get(:@items) +end +``` + +The test passes, but it locks you into the array representation. The day you decide `@items` should be a hash keyed by SKU, the test breaks even though the behavior is identical. The corrected version asserts on the public surface: + +```ruby +# Fix: tests behavior +def test_add_increases_total_by_item_price + order = Order.new + order.add(price: 1000) + assert_equal 1000, order.total +end +``` + +The behavior - "adding an item with price 1000 makes the total 1000" - is what the rest of your code actually depends on. That's the contract. The internal storage is implementation detail and should be free to change. + +The second mistake is skipping the refactor step entirely. Shameless Green is supposed to be the cheap, embarrassing first version - not the final form. If cycle 4's `@items << price * quantity` is still your storage strategy by cycle 12, when you're adding discounts and tax codes and currency, you'll have crammed seven concerns into one integer. The refactor step is where you take the four examples you now have and let a `LineItem` value object emerge, because by cycle 4 you've earned the right to that abstraction. [Refactoring with the existing tests as a safety net](/blog/test-driven-thinking-for-solving-common-ruby-pitfalls-rails-tdd/) is a separate skill from adding behavior, and it's the half of TDD that pays the long-term dividend. + +The third mistake is bundling tidy and behavior commits. We covered the mechanics in the previous section, and it's worth naming as a TDD failure mode in its own right: a developer who skips Tidy First loses the ability to revert just the behavior change. The git log reads like prose paragraphs - lots of words, no clear claims. Strictly speaking that's a Tidy First problem rather than a TDD problem, but the two are inseparable in practice. The moment your commit log stops working as a reliable rollback target, the 90-second loop loses its safety net and you quietly stop trusting it. + +## How JetThoughts uses TDD on rescues + +When we inherit a codebase, the three failure modes above are usually all present at once - tests coupled to internals, refactor steps skipped for months, commits that mix tidy with behavior. We rescue Ruby on Rails projects from devshops that shipped this exact configuration with a CI suite that takes 22 minutes to run. The 90-second loop is the discipline we put back first. + +Forty-plus rescue engagements over seventeen years, and the pattern is consistent. Inherited codebases either have no tests at all (the last three we picked up) or have a test suite written after the fact - exhaustive, brittle, mocked-to-the-teeth, and useless under change pressure. We rebuild the rhythm first. Then we fix the bugs. [Refactoring callbacks back into services](/blog/how-avoid-callbacks-using-services-rails-refactoring/) and tightening the test suite go hand in hand once the cadence is in place. + +If you're holding a Rails codebase you can't change without breaking, we run a free 45-minute audit: one senior developer reads your suite and your most recent five PRs, and you get a one-page written assessment naming the three fixes that would help most. No contract, no follow-up sales call. + +[Talk to us about your codebase](/contact-us/). + +## Further reading + +- [Sandi Metz, *99 Bottles of OOP*](https://sandimetz.com/99bottles) - Shameless Green and the Flocking Rules +- [Kent Beck, *Tidy First?* (2023)](https://tidyfirst.substack.com/) - structural vs behavioral changes +- [J.B. Rainsberger, "The Myth of Advanced TDD"](https://blog.thecodewhisperer.com/permalink/the-myth-of-advanced-tdd-is-a-symptom) - the sit-up analogy and "tests are the cries" +- [The Agile Institute, "Dispelling Myths About TDD"](https://agileinstitute.com/articles/dispelling-myths-about-tdd) - the time-ledger framing + +Related: [why and how to use TDD](/blog/why-how-use-tdd-main-tips-testing/), [test-driven thinking for Ruby pitfalls](/blog/test-driven-thinking-for-solving-common-ruby-pitfalls-rails-tdd/), [how mocks sink TDD](/blog/mock-everything-good-way-sink-tdd-testing/), [refactoring callbacks into services](/blog/how-avoid-callbacks-using-services-rails-refactoring/).