Use when writing Rails tests - enforces fixtures-only, integration-style controller tests, Current context setup, simple naming, and Minitest syntax
Enforces 37signals' vanilla Rails testing style: fixtures-only, integration-style controller tests, Current.context setup, and Minitest syntax. Triggers when writing tests to prevent RSpec, FactoryBot, and data creation.
/plugin marketplace add ZempTime/zemptime-marketplace/plugin install vanilla-rails@zemptime-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Counter-intuitive patterns from 37signals. These are NOT standard Rails community practices.
Core principle: Fixtures over factories. Always. Integration tests for controllers. Minitest, not RSpec.
NO FACTORYBOT. NO RSPEC. NO DATA CREATION IN TESTS.
If you write create(:model), FactoryBot, let, describe, it, expect, or before_each → DELETE IT.
# ✅ GOOD - Reference fixtures
test "close card" do
cards(:logo).close
assert cards(:logo).closed?
end
# ❌ BAD - Never create data
test "close card" do
card = create(:card) # DELETE THIS
card.close
end
# ❌ BAD - Never use FactoryBot
let(:card) { create(:card) } # DELETE THIS
# ❌ BAD - Never use RSpec
describe Card do # DELETE THIS
it "closes" do # DELETE THIS
Reference fixtures by symbol: cards(:logo), users(:david), boards(:writebook), sessions(:kevin)
Why fixtures? They're real data, loaded once, fast, and force you to think about realistic scenarios.
Q: What if the fixture I need doesn't exist? A: Add it to the fixtures file. Never create data in tests.
# ✅ GOOD - ActionDispatch::IntegrationTest
class Cards::ClosuresControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in_as :kevin
end
test "create" do
post card_closure_path(cards(:logo)), as: :turbo_stream
assert_response :success
end
end
# ❌ BAD - Never use ActionController::TestCase
class Cards::ClosuresControllerTest < ActionController::TestCase # DELETE THIS
post :create, params: { card_id: card.id } # DELETE THIS
end
Why integration style? Tests the full request cycle including routing, middleware, and response rendering.
# ✅ GOOD - Set Current.session in setup
class Card::CloseableTest < ActiveSupport::TestCase
setup do
Current.session = sessions(:david)
end
test "close records user" do
cards(:logo).close(user: users(:kevin))
assert_equal users(:kevin), cards(:logo).closed_by
end
end
# ❌ BAD - Missing Current.session
class Card::CloseableTest < ActiveSupport::TestCase
# Missing setup - tests may fail or behave incorrectly
test "close records user" do
cards(:logo).close(user: users(:kevin))
end
end
Why Current.session? Models often rely on Current.session for user context, event recording, and authorization.
Always set it, even if you think it's not needed.
# ✅ GOOD - Concise, clear
test "create"
test "close records user"
test "reopen creates event"
# ❌ BAD - Verbose, repetitive
test "should create a new card when given valid parameters"
test "should mark the card as closed when user closes it"
it "should record the user who closed the card" # Also wrong syntax
Why simple names? File and test method name provide enough context. No need to repeat.
# ✅ GOOD - Minitest assertions
test "close" do
assert cards(:logo).close
assert cards(:logo).closed?
assert_not cards(:shipping).open?
end
# ✅ GOOD - Exception testing
assert_raises ActiveRecord::RecordNotFound do
Card.find("nonexistent")
end
# ❌ BAD - RSpec syntax
it "should close" do # DELETE THIS
expect(card.close).to be_truthy # DELETE THIS
expect(card.closed?).to be true # DELETE THIS
end
# ❌ BAD - RSpec describe/context
describe "#close" do # DELETE THIS
context "when card is open" do # DELETE THIS
Use: test, setup, assert, assert_equal, assert_not, assert_difference, assert_changes, assert_raises
NEVER use: it, describe, context, before_each, let, expect, refute (use assert_not instead)
# ✅ GOOD - Single change with lambda syntax
assert_difference -> { Card.count }, +1 do
post board_cards_path(boards(:writebook))
end
# ✅ GOOD - Multiple changes
assert_difference({
-> { cards(:logo).events.count } => +1,
-> { Event.count } => +1
}) do
cards(:logo).close(user: users(:kevin))
end
# ❌ BAD - RSpec expect/to change
expect { # DELETE THIS
card.close
}.to change { Event.count }.by(1) # DELETE THIS
Why lambda syntax? Evaluates the expression in the block's context, capturing state changes correctly.
# ✅ GOOD - Boolean state changes
assert_changes -> { cards(:logo).reload.closed? }, from: false, to: true do
post card_closure_path(cards(:logo)), as: :turbo_stream
end
# ❌ BAD - Manual before/after checks
closed_before = card.closed? # Too verbose
card.close
assert_not_equal closed_before, card.reload.closed?
System tests follow the same rules:
# ✅ GOOD - ApplicationSystemTestCase with fixtures
class SmokeTest < ApplicationSystemTestCase
test "create a card" do
sign_in_as(users(:david))
visit board_url(boards(:writebook))
click_on "Add a card"
# ...
end
end
# ❌ BAD - Creating data in system tests
test "create a card" do
user = create(:user) # DELETE THIS
sign_in_as(user)
end
Use mocha for stubbing, webmock for HTTP requests:
# ✅ GOOD - Mocha for stubbing
TestMailer.stubs(:goes_boom).raises(Net::SMTPSyntaxError)
# ✅ GOOD - WebMock for HTTP
stub_request(:post, webhook.url).to_return(status: 200)
# ❌ BAD - RSpec mocks
allow(TestMailer).to receive(:goes_boom) # DELETE THIS
| Excuse | Reality |
|---|---|
| "FactoryBot is standard Rails practice" | Not in vanilla Rails. Fixtures only. |
| "RSpec is more expressive" | Minitest is simpler. Use it. |
| "Creating test data makes tests clearer" | Fixtures make tests realistic. No creation. |
| "ActionController::TestCase is for controllers" | Integration tests cover more. Use those. |
| "Current.session isn't needed here" | Models rely on it. Always set it. |
| "Verbose names document the test" | File name + method name = enough context. |
| "let makes setup DRY" | setup method does the same. No let. |
| "describe/context organizes tests" | Class and file organization is enough. |
| "I'll just create one record since fixture missing" | Add the fixture. Never create in tests. |
| "refute is more idiomatic Minitest" | Use assert_not for consistency. |
Seeing any of these? DELETE THE CODE and start over:
create(:model), build(:model), or FactoryBotlet, describe, it, context, before_each, subjectexpect(...).to or .shouldActionController::TestCaseCurrent.session = sessions(:fixture)User.create, Card.new, Board.build)refute (use assert_not)| Pattern | Use | Never Use |
|---|---|---|
| Test framework | Minitest | RSpec |
| Test data | Fixtures (cards(:logo)) | Factories (create(:card)) |
| Controller tests | ActionDispatch::IntegrationTest | ActionController::TestCase |
| System tests | ApplicationSystemTestCase | Any other base class |
| Model setup | Current.session = sessions(:david) | Nothing (missing context) |
| Test blocks | test "name" | it "should...", describe |
| Setup | setup do | before_each, let, subject |
| Assertions | assert, assert_not | expect, should, refute |
| Test names | test "create" | it "should create..." |
| Stubbing | mocha (.stubs) | RSpec mocks (allow, expect) |
| HTTP mocking | webmock | VCR, other tools |
require "rails_helper"
RSpec.describe Card, type: :model do
let(:board) { create(:board) }
let(:card) { create(:card, board: board) }
let(:user) { create(:user) }
describe "#close" do
it "should mark card as closed when user closes it" do
expect {
card.close(user: user)
}.to change { card.reload.closed? }.from(false).to(true)
expect(card.closed_by).to eq(user)
end
context "when already closed" do
let(:card) { create(:card, :closed) }
it "should not create duplicate events" do
expect {
card.close(user: user)
}.not_to change { Event.count }
end
end
end
end
require "test_helper"
class Card::CloseableTest < ActiveSupport::TestCase
setup do
Current.session = sessions(:david)
end
test "close" do
assert_not cards(:logo).closed?
cards(:logo).close(user: users(:kevin))
assert cards(:logo).closed?
assert_equal users(:kevin), cards(:logo).closed_by
end
test "close creates event" do
assert_difference -> { cards(:logo).events.count }, +1 do
cards(:logo).close(user: users(:kevin))
end
end
end
Vanilla Rails is deliberately simple:
If the Rails community does it differently, that's fine. We don't.
Follow these patterns exactly. No exceptions, no "better" alternatives.
Master defensive Bash programming techniques for production-grade scripts. Use when writing robust shell scripts, CI/CD pipelines, or system utilities requiring fault tolerance and safety.