Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
dchuk avatar

Tdd Cycle

  • 2 installs
  • Updated February 9, 2026
  • dchuk/rails_ai_agents

Guides the Red-Green-Refactor TDD workflow in Rails using Minitest and fixtures (never RSpec or FactoryBot).

About

Drives Test-Driven Development with a Red-Green-Refactor cycle using Minitest and fixtures. A developer uses it when implementing a feature test-first or following TDD practices.

  • Red-Green-Refactor cycle with Minitest
  • Fixtures over RSpec/FactoryBot

Tdd Cycle by the numbers

  • 2 all-time installs (skills.sh)
  • Ranked #1,693 of 2,153 Testing & QA skills by installs in the Skillselion catalog
  • Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dchuk/rails_ai_agents --skill tdd-cycle

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs2
Last updatedFebruary 9, 2026
Repositorydchuk/rails_ai_agents

What it does

Guides the Red-Green-Refactor TDD workflow in Rails using Minitest and fixtures (never RSpec or FactoryBot).

Files

SKILL.mdMarkdownGitHub ↗

TDD Cycle — Minitest + Fixtures

Project Conventions

  • Testing: Minitest + fixtures (NEVER RSpec or FactoryBot)
  • Components: ViewComponents for reusable UI (partials OK for simple one-offs)
  • Authorization: Pundit policies (deny by default)
  • Jobs: Solid Queue, shallow jobs, _later/_now naming
  • Frontend: Hotwire (Turbo + Stimulus) + Tailwind CSS
  • State: State-as-records for business state (booleans only for technical flags)
  • Architecture: Rich models first, service objects for multi-model orchestration
  • Routing: Everything-is-CRUD (new resource over new action)
  • Quality: RuboCop (omakase) + Brakeman

The Cycle

1. RED    → Write a failing test that describes desired behavior
2. GREEN  → Write the minimum code to pass the test
3. REFACTOR → Improve code while keeping tests green
4. REPEAT → Next behavior

Workflow Checklist

TDD Progress:
- [ ] Step 1: Understand the requirement
- [ ] Step 2: Choose test type (model/controller/system/component)
- [ ] Step 3: Write failing test (RED)
- [ ] Step 4: Verify test fails correctly
- [ ] Step 5: Implement minimal code (GREEN)
- [ ] Step 6: Verify test passes
- [ ] Step 7: Refactor if needed
- [ ] Step 8: Verify tests still pass

Step 1: Requirement Analysis

Before writing any code, understand:

  • What is the expected input?
  • What is the expected output/behavior?
  • What are the edge cases?
  • What errors should be handled?

Step 2: Choose Test Type

Test TypeUse ForLocation
Model testValidations, scopes, instance methodstest/models/
Controller testHTTP flow, authorization, responsestest/controllers/
System testFull user flows with JavaScripttest/system/
Service testBusiness logic, complex operationstest/services/
Query testComplex queries, correctnesstest/queries/
Component testViewComponent renderingtest/components/
Policy testPundit authorization rulestest/policies/
Job testBackground job behaviortest/jobs/
Mailer testEmail content, recipientstest/mailers/

Step 3: Write Failing Test (RED)

Model Test Template

# test/models/post_test.rb
require "test_helper"

class PostTest < ActiveSupport::TestCase
  setup do
    @post = posts(:published)
  end

  test "requires title" do
    @post.title = nil
    assert_not @post.valid?
    assert_includes @post.errors[:title], "can't be blank"
  end

  test ".recent returns posts in descending order" do
    recent = posts(:recent)
    old = posts(:old)
    assert_equal [recent, old], Post.recent.to_a
  end

  test "#publish! creates a publication record" do
    post = posts(:draft)
    assert_difference "Publication.count", 1 do
      post.publish!(user: users(:admin))
    end
  end
end

Controller (Integration) Test Template

# test/controllers/posts_controller_test.rb
require "test_helper"

class PostsControllerTest < ActionDispatch::IntegrationTest
  setup do
    @user = users(:one)
    @post = posts(:one)
    sign_in_as @user
  end

  test "should get index" do
    get posts_url
    assert_response :success
  end

  test "should create post" do
    assert_difference("Post.count") do
      post posts_url, params: { post: { title: "New Post", body: "Content" } }
    end
    assert_redirected_to post_url(Post.last)
  end

  test "should not create post with invalid params" do
    assert_no_difference("Post.count") do
      post posts_url, params: { post: { title: "", body: "" } }
    end
    assert_response :unprocessable_entity
  end

  test "requires authentication" do
    sign_out
    get posts_url
    assert_redirected_to new_session_url
  end
end

Service Test Template

# test/services/orders/create_service_test.rb
require "test_helper"

class Orders::CreateServiceTest < ActiveSupport::TestCase
  setup do
    @user = users(:one)
    @product = products(:widget)
    @service = Orders::CreateService.new
  end

  test "creates order with valid params" do
    result = @service.call(user: @user, items: [{ product_id: @product.id, quantity: 2 }])

    assert result.success?
    assert_kind_of Order, result.data
    assert_equal @user, result.data.user
  end

  test "returns failure with empty items" do
    result = @service.call(user: @user, items: [])

    assert result.failure?
    assert_equal :empty_cart, result.code
  end

  test "wraps in transaction" do
    assert_no_difference "Order.count" do
      @service.call(user: @user, items: [{ product_id: 0, quantity: 1 }])
    end
  end
end

System Test Template

# test/system/posts_test.rb
require "application_system_test_case"

class PostsTest < ApplicationSystemTestCase
  setup do
    @user = users(:one)
    sign_in_as @user
  end

  test "creating a post" do
    visit new_post_url

    fill_in "Title", with: "My Post"
    fill_in "Body", with: "Post content here"
    click_button "Create Post"

    assert_text "Post created successfully"
    assert_text "My Post"
  end

  test "shows validation errors" do
    visit new_post_url
    click_button "Create Post"

    assert_text "can't be blank"
  end
end

ViewComponent Test Template

# test/components/status_badge_component_test.rb
require "test_helper"

class StatusBadgeComponentTest < ViewComponent::TestCase
  test "renders published badge" do
    render_inline(StatusBadgeComponent.new(status: :published))

    assert_selector ".badge", text: "Published"
    assert_selector ".bg-green-100"
  end

  test "renders draft badge" do
    render_inline(StatusBadgeComponent.new(status: :draft))

    assert_selector ".badge", text: "Draft"
    assert_selector ".bg-gray-100"
  end
end

Policy Test Template

# test/policies/post_policy_test.rb
require "test_helper"

class PostPolicyTest < ActiveSupport::TestCase
  setup do
    @owner = users(:one)
    @other = users(:two)
    @post = posts(:one) # belongs to @owner
  end

  test "owner can update" do
    assert PostPolicy.new(@owner, @post).update?
  end

  test "non-owner cannot update" do
    assert_not PostPolicy.new(@other, @post).update?
  end

  test "scope returns only authorized records" do
    scope = PostPolicy::Scope.new(@owner, Post).resolve
    assert_includes scope, @post
  end
end

Step 4: Verify Failure

Run the test:

bin/rails test test/models/post_test.rb --verbose

The test MUST fail with a clear error. If it passes immediately, either:

  • The behavior already exists
  • The test isn't testing what you think

Step 5: Implement (GREEN)

Write the MINIMUM code to pass:

  • No optimization
  • No edge case handling beyond what's tested
  • No refactoring
  • Just make it work

Step 6: Verify Pass

bin/rails test test/models/post_test.rb --verbose

Step 7: Refactor

Improve code while tests stay green:

  • Extract methods for clarity
  • Improve naming
  • Remove duplication
  • Simplify logic

Rule: Make ONE change at a time, run tests after EACH change.

Step 8: Final Verification

Run all related tests:

bin/rails test

Fixtures Best Practices

# test/fixtures/posts.yml
published:
  title: Published Post
  body: This is published content
  user: one
  created_at: <%= 1.day.ago %>

draft:
  title: Draft Post
  body: This is draft content
  user: one

recent:
  title: Recent Post
  body: Recent content
  user: one
  created_at: <%= 1.hour.ago %>

old:
  title: Old Post
  body: Old content
  user: two
  created_at: <%= 1.year.ago %>

Fixture naming tips:

  • Use descriptive names: published, draft, admin_post
  • Reference other fixtures by name: user: one
  • Use ERB for dynamic values: <%= Time.current %>

Test Helper Patterns

# test/test_helper.rb
class ActiveSupport::TestCase
  # Use fixtures for all tests
  fixtures :all

  # Authentication helper
  def sign_in_as(user)
    post session_url, params: { email: user.email, password: "password" }
  end

  def sign_out
    delete session_url
  end
end

Anti-Patterns to Avoid

1. Testing implementation, not behavior — Test what it does, not how 2. Too many assertions per test — One concept per test 3. Brittle tests — Don't assert exact timestamps or error messages 4. Slow tests — Prefer model tests over system tests when possible 5. Skipping the RED step — Always see it fail first 6. Over-mocking — Use real objects with fixtures when possible

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.