Quality & TestingDevOps

Test && Commit || Revert: Why Architects Should Master TCR

OL
Oscar van der Leij
12 min read
Test && Commit || Revert: Why Architects Should Master TCR

It was 2:47 AM when Sarah, a principal architect at a rapidly scaling SaaS company, finally admitted defeat. She’d been debugging for six hours, trying to untangle a "quick refactoring" that had somehow metastasized into 847 lines of changed code across 23 files. The tests were failing in mysterious ways, her Git diff looked like a Jackson Pollock painting, and she couldn’t remember which of her "improvements" had actually broken things. Sound familiar?

The problem is that we’re too good at convincing ourselves we can hold complex state trees in our heads. We architect elegant systems with careful boundaries and clean abstractions, yet when we code, we often work in sprawling, multi-hour sessions that would make our younger selves cringe. We know better, but somewhere between "I’ll just refactor this one class" and "why is everything on fire," we lose the plot.

Understanding TCR: The Nuclear Option for Discipline

Think of Test && Commit || Revert as a particularly stern driving instructor who grabs the wheel the moment you drift out of your lane. Not to punish you, but because staying in your lane is literally the only way to reach your destination safely.

TCR is deceptively simple: After every code change, you run your tests. If they pass, your changes are automatically committed. If they fail, your changes are automatically reverted, gone, deleted, ctrl-Z’d into oblivion. No second chances, no "just let me fix this one thing," no negotiation.

The genius is in how this brutal feedback loop rewires your brain. Within hours of using TCR, you start thinking differently. Your changes become smaller. Your tests become faster. Your commits become more frequent. You stop trying to boil the ocean and start making incremental, verifiable progress.

Code ChangeRun TestsCommit
Pass/Fail?
Pass/Fail?
RevertPassFail

TCR emerged from a 2018 Code Camp in Oslo, where Kent Beck, Lars Barlindhaug, Oddmund Strømme, and Ole Johannessen were experimenting with extreme programming workflows. Kent Beck had proposed "test && commit": automatically committing when tests pass. Then Oddmund Strømme suggested the brutal addition: if tests fail, revert. Beck later wrote that he "hated the idea so much he had to try it." The && means "and then" (tests pass, so commit), while || means "or else" (tests failed, so revert). It's a workflow that makes discipline automatic rather than aspirational.

The Psychology Behind the Pain

What makes TCR fascinating from an architectural perspective is that it’s about cognitive load management and feedback loops.

Traditional TDD asks us to write tests first, then make them pass. Excellent in theory, but in practice, we often write a test, then spend 20 minutes implementing a feature, then spend another 10 minutes debugging why the test fails. During that 30-minute window, we’ve made dozens of micro-decisions, introduced multiple changes, and created a tangled web of cause and effect.

TCR compresses that feedback loop to seconds. You make a tiny change, run the tests, and get immediate, binary feedback: keep going or start over. The psychological impact is profound:

Traditional Development TCR Development
Large, complex changes Atomic, minimal changes
Delayed feedback (minutes to hours) Immediate feedback (seconds)
Debugging as archaeology Debugging as memory recall
Commits as checkpoints Commits as heartbeats
Fear of losing work encourages larger changes Acceptance of reversion encourages smaller changes

The first time your changes get reverted, you’ll feel a spike of panic. "But I was so close!" The second time, you’ll feel annoyed. By the tenth time, you’ll start planning smaller changes. By the hundredth, you’ll wonder how you ever worked any other way.

The Architect’s Secret Weapon: YAGNI on Steroids

As architects, we’re trained to think ahead, to anticipate future needs, to build extensible systems. This is valuable, but it’s also dangerous. We’ve all seen (or built) that framework that was "designed for future scalability" but never actually got used beyond its initial purpose.

TCR is YAGNI (You Aren’t Gonna Need It) with teeth. When you know that any failing test will eliminate your work, you stop adding "just in case" features. You stop building abstractions for hypothetical future requirements. You focus ruthlessly on the next smallest thing that could possibly work.

Consider this scenario: You’re adding a new feature to an API gateway. In traditional development, you might:

  1. Design the interface for the new feature
  2. Implement the core logic
  3. Add error handling
  4. Add logging
  5. Add metrics
  6. Add configuration options
  7. Write tests
  8. Debug everything at once

With TCR, this becomes:

  1. Write test + simplest implementation together (commit)
  2. Write error handling test + implementation together (commit)
  3. Write logging test + implementation together (commit)
  4. And so on...

Each step is verified, committed, and safe. Remember: there’s no "red" phase in TCR. You can’t save a failing test. It will be reverted. Every save must include both the test and the code that makes it pass. If you get clever and try to add "just a little extra," the revert hammer comes down.

Implementing TCR in .NET

Let’s set up TCR for a .NET project. First, create a solution structure:

# Create solution and projects
dotnet new gitignore
dotnet new sln -n TcrDemo
dotnet new classlib -n TcrDemo.Core -o src/TcrDemo.Core
dotnet new xunit -n TcrDemo.Tests -o tests/TcrDemo.Tests

# Add projects to solution
dotnet sln add src/TcrDemo.Core
dotnet sln add tests/TcrDemo.Tests

# Add reference from tests to core
dotnet add tests/TcrDemo.Tests reference src/TcrDemo.Core

# Initialize git (TCR requires version control)
git init
git add -A
git commit -m "Initial project setup"

The Simple Approach: Manual TCR

TCR in its purest form is just a shell command. After making changes, run:

# Bash / PowerShell 7+
dotnet test && git commit -am "TCR" || git checkout .

This is Kent Beck’s original formulation: test, and if tests pass then commit, or else revert.

For Windows PowerShell 5.1 (the default on Windows), the && and || operators aren’t supported. Use this function instead:

# Add to your PowerShell profile
function tcr {
    dotnet test
    if ($LASTEXITCODE -eq 0) {
        git add -A
        git commit -m "TCR" --no-verify
    } else {
        git checkout .
    }
}

Now after each change, just type tcr. Simple, no tooling required.

The Automated Approach: File Watcher

For a more immersive TCR experience, use a file watcher that runs TCR automatically on save. Here’s a PowerShell script (tcr.ps1) that watches both source and test files:

# tcr.ps1 - Automated TCR for .NET projects
param(
    [string]$TestProject = "tests\TcrDemo.Tests"
)

Write-Host "TCR Active - Watching for .cs file changes" -ForegroundColor Yellow
Write-Host "Press Ctrl+C to exit`n" -ForegroundColor Yellow

$lastCheck = Get-Date

while ($true) {
    Start-Sleep -Milliseconds 500

    # Find most recently modified .cs file
    $latest = Get-ChildItem -Path . -Filter "*.cs" -Recurse -ErrorAction SilentlyContinue |
              Where-Object { $_.FullName -notmatch '\\obj\\|\\bin\\' } |
              Sort-Object LastWriteTime -Descending |
              Select-Object -First 1

    if ($latest -and $latest.LastWriteTime -gt $lastCheck) {
        $lastCheck = Get-Date

        Write-Host "`nChange detected: $($latest.Name)" -ForegroundColor Cyan
        Write-Host "Running TCR..." -ForegroundColor Cyan

        # Build
        dotnet build --verbosity quiet 2>$null
        if ($LASTEXITCODE -ne 0) {
            Write-Host "Build failed - reverting" -ForegroundColor Red
            git checkout .
            continue
        }

        # Test
        dotnet test $TestProject --no-build --verbosity quiet 2>$null
        if ($LASTEXITCODE -eq 0) {
            git add -A
            git commit -m "TCR" --no-verify 2>$null
            Write-Host "Tests passed - committed" -ForegroundColor Green
        } else {
            git checkout .
            Write-Host "Tests failed - reverted" -ForegroundColor Red
        }
    }
}

Run it from your solution root:

.\tcr.ps1 -TestProject "tests\TcrDemo.Tests"

TCR in Action: A Worked Example

Let’s build a simple StringCalculator class using TCR to see the workflow in practice. The critical difference from traditional TDD: there is no "red" phase. Every save must pass all tests, or your changes disappear.

The TDD Habit That TCR Breaks

If you’re coming from TDD, your instinct is to write a failing test first. Let’s see what happens:

// StringCalculatorTests.cs - You write a test first
[Fact]
public void Add_EmptyString_ReturnsZero()
{
    var calculator = new StringCalculator();
    var result = calculator.Add("");
    Assert.Equal(0, result);
}

You save. TCR runs tests. StringCalculator doesn’t exist → test fails → REVERT. Your test is gone.

This is TCR’s first lesson: the "watch it fail" phase doesn’t exist. You cannot save a failing test.

The TCR Way: Test and Implementation Together

With TCR, you write test and implementation as one atomic change:

// StringCalculator.cs
public class StringCalculator
{
    public int Add(string numbers) => 0;
}
// StringCalculatorTests.cs
[Fact]
public void Add_EmptyString_ReturnsZero()
{
    var calculator = new StringCalculator();
    var result = calculator.Add("");
    Assert.Equal(0, result);
}

You save both files. TCR runs tests. Tests pass → COMMIT.

You now have working, tested code. The test validates behavior, the implementation satisfies the test, all committed in one atomic change.

Adding Functionality: The Revert Teaches You

Time to handle single numbers. Old habits die hard, so you add just the test:

[Fact]
public void Add_SingleNumber_ReturnsThatNumber()
{
    var calculator = new StringCalculator();
    Assert.Equal(5, calculator.Add("5"));
}

You save. Test fails (returns 0 instead of 5) → REVERT. Test gone.

Now you do it the TCR way, adding test and implementation together:

// StringCalculator.cs - Updated method
public int Add(string numbers)
{
    if (string.IsNullOrEmpty(numbers)) return 0;
    return int.Parse(numbers);
}
// StringCalculatorTests.cs - New test
[Fact]
public void Add_SingleNumber_ReturnsThatNumber()
{
    var calculator = new StringCalculator();
    Assert.Equal(5, calculator.Add("5"));
}

You save. All tests pass → COMMIT.

The Pattern Emerges

Action Result Lesson
Save test without implementation REVERT No "red" phase in TCR
Save test + minimal implementation COMMIT Atomic, working increments
Save test without updating implementation REVERT Forces you to think before saving
Save test + implementation together COMMIT Every commit is a complete feature slice

Every commit represents complete, working code. You literally cannot have failing tests in your repository because TCR won’t let you save them. This eliminates "I’ll fix it later" and "it works on my machine" entirely.

When TCR Hurts And Why That’s the Point

TCR is uncomfortable. The first day you use it, you’ll probably want to throw your laptop out the window. Here’s what you’ll encounter:

The Revert Rage: You’ll lose code. Code you were proud of. Code that was "almost working." This is by design. The pain teaches you to work in smaller increments.

The Test Speed Tax: If your tests take 30 seconds to run, TCR becomes torture. This forces you to confront test performance issues you’ve been ignoring. Fast tests aren’t optional anymore.

The Humility Hammer: TCR exposes how often we write code that doesn’t actually work. When every change must pass tests, you can’t hide behind "I’ll fix it later" or "It works on my machine."

These are the features of TCR. The discomfort is the point. It’s like a form checker at the gym who won’t let you progress to heavier weights until your technique is perfect.

TCR Meets GenAI: A Match Made in Heaven (Or Hell)

Here’s where things get really interesting for architects: TCR might be the perfect workflow for AI-assisted development.

When you’re using GitHub Copilot, ChatGPT, Claude Code, or other AI coding assistants, you face a new problem: the AI can generate large amounts of code quickly, but that code might be subtly wrong in ways that aren’t immediately obvious. TCR provides a safety net.

The workflow with AI becomes:

  1. Describe what you need to the AI
  2. Ask it to generate both the test AND the implementation together
  3. Paste each into the appropriate file in your codebase
  4. Save → if all tests pass (existing + new), commit; if any test fails, revert
  5. If reverted, analyze what went wrong and refine your prompt
  6. Repeat until TCR commits your change
// OrderProcessor.cs
public class OrderProcessor
{
    public decimal CalculateTotal(Order order)
    {
        var subtotal = order.Items.Sum(i => i.Price * i.Quantity);
        var tax = subtotal * order.TaxRate;
        return subtotal + tax;
    }
}
// OrderProcessorTests.cs
[Fact]
public void CalculateTotal_WithStandardOrder_ReturnsCorrectTotal()
{
    var processor = new OrderProcessor();
    var order = new Order
    {
        Items = new[] { new OrderItem { Price = 10m, Quantity = 2 } },
        TaxRate = 0.1m
    };

    var result = processor.CalculateTotal(order);

    Assert.Equal(22m, result); // 20 + 2 tax
}

We’re pasting each snippet into its respective file, then save. If the AI’s implementation is correct, TCR commits it. If there’s a bug, everything reverts and you ask the AI to try again with more context about what went wrong. This prevents AI-generated code from accumulating into a tangled mess. Each AI suggestion is validated immediately or discarded.

Small Steps, Giant Leaps

TCR isn’t for everyone, and it isn’t for every project. But as architects, we should care about it for three reasons:

  • It enforces the discipline we preach: We tell teams to work incrementally, to write tests, to commit often. TCR makes these practices automatic rather than aspirational.
  • It exposes systemic problems: Slow tests, tight coupling, poor abstractions. TCR makes these issues painful enough that they get fixed.
  • It’s a forcing function for quality: In an era where we’re experimenting with AI-assisted development, having an automatic quality gate becomes increasingly valuable.

The real question is "What would my codebase look like if every change had to pass tests immediately or be discarded?" Try TCR for a week on a side project, because experiencing that level of discipline will change how you think about all development. You might not adopt it permanently, but you’ll never write code quite the same way again.

What would happen to your current project if you enabled TCR right now? Which parts of your codebase would survive, and which would need to be rethought? And what does that tell you about the real state of your architecture?

Share this article

Enjoyed this article?

Subscribe to get more insights delivered to your inbox monthly

Subscribe to Newsletter