
rails-testing
by XxSachaxX
A web app designed to help bring people together to start and evolve all types of projects
SKILL.md
name: rails-testing description: Write RSpec tests for Co-Create Rails app following project conventions. Use when writing model tests, request tests, or testing features like authentication, authorization, validations, associations, scopes, callbacks, or API endpoints. allowed-tools: Read, Write, Edit, Bash, Grep, Glob
Rails Testing Skill for Co-Create
This skill helps write RSpec tests that match the Co-Create project's testing conventions and style.
Test Structure Conventions
Model Tests
Use describe blocks to group related tests by method or feature:
require 'rails_helper'
RSpec.describe ModelName, type: :model do
describe "#method_name" do
describe "when specific condition" do
let!(:resource) { FactoryBot.create(:resource) }
it "describes expected behavior" do
expect(resource.method_name).to eq(expected_value)
end
end
describe "when different condition" do
it "describes different behavior" do
# test implementation
end
end
end
describe "associations" do
let(:model) { FactoryBot.create(:model) }
it "has many associated_models" do
expect(model).to respond_to(:associated_models)
end
end
describe "validations" do
describe "when field is nil" do
let(:model) { FactoryBot.build(:model, field: nil) }
it "is invalid" do
expect(model).not_to be_valid
expect(model.errors[:field]).to include("can't be blank")
end
end
end
describe "callbacks" do
describe "callback_name" do
describe "when condition applies" do
let(:model) { FactoryBot.create(:model, field: "Value") }
it "transforms the data appropriately" do
expect(model.field).to eq("transformed_value")
end
end
end
end
describe "scopes" do
describe ".scope_name" do
let!(:matching_record) { FactoryBot.create(:model, status: "active") }
let!(:non_matching_record) { FactoryBot.create(:model, status: "inactive") }
it "returns records matching the scope criteria" do
results = Model.scope_name
expect(results).to include(matching_record)
expect(results).not_to include(non_matching_record)
end
end
end
end
Request Tests
Use describe blocks for HTTP verbs and paths, test authentication and authorization:
require 'rails_helper'
RSpec.describe "ResourceName", type: :request do
describe "GET /path" do
let!(:user) { FactoryBot.create(:user) }
before { sign_in user }
describe "when condition applies" do
it "returns successful response" do
get resource_path
expect(response).to have_http_status(:success)
expect(response.body).to include("Expected Content")
end
end
end
describe "POST /path" do
let!(:user) { FactoryBot.create(:user) }
before { sign_in user }
describe "when creating valid resource" do
it "creates the resource" do
expect {
post resources_path, params: {
resource: {
field: "value"
}
}
}.to change { Resource.count }.by(1)
end
end
end
describe "DELETE /path/:id" do
let!(:owner) { FactoryBot.create(:user) }
let!(:resource) { FactoryBot.create(:resource) }
before { sign_in owner }
describe "when the user is authorized" do
it "deletes the resource" do
expect { delete resource_path(resource) }.
to change { Resource.count }.by(-1)
end
end
describe "when the user is not authorized" do
let!(:other_user) { FactoryBot.create(:user) }
before { sign_in other_user }
it "does not delete the resource" do
expect { delete resource_path(resource) }.
not_to change { Resource.count }
end
end
end
end
FactoryBot Conventions
Factory Usage
- Always use
FactoryBot.createfor records that need persistence - Always use
FactoryBot.buildfor records that don't need persistence (e.g., testing validations) - Use
let!(with bang) when you need the record to exist before the test runs - Use
let(without bang) when lazy evaluation is acceptable
# Persisted record needed for associations or database queries
let!(:user) { FactoryBot.create(:user) }
# Build for validation tests (doesn't need to be saved)
let(:invalid_user) { FactoryBot.build(:user, email_address: nil) }
# Lazy evaluation is fine
let(:project) { FactoryBot.create(:project) }
Factory Attributes
Use Faker for dynamic data in factories:
FactoryBot.define do
factory :user do
name { Faker::Name.name }
email_address { Faker::Internet.email }
password { "password" }
end
factory :project do
name { Faker::Lorem.word }
description { "A very long description that exceeds the maximum length" }
end
factory :tag do
sequence(:name) { |n| "tag#{n}" }
projects_count { 0 }
end
end
Factory Traits
Use traits for variations:
factory :project do
name { Faker::Lorem.word }
description { "Long description" }
trait :with_tags do
transient do
tag_count { 3 }
end
after(:create) do |project, evaluator|
FactoryBot.create_list(:tag, evaluator.tag_count).each do |tag|
project.tags << tag
end
end
end
end
# Usage
let(:project_with_tags) { FactoryBot.create(:project, :with_tags) }
let(:project_with_5_tags) { FactoryBot.create(:project, :with_tags, tag_count: 5) }
Overriding Factory Attributes
Pass attributes directly to override defaults:
let!(:project) { FactoryBot.create(:project, name: "Specific Name") }
let!(:owner_membership) {
FactoryBot.create(
:project_membership,
user: owner,
project: project,
role: "owner",
status: "active"
)
}
Authentication Helper
Use sign_in helper for request tests (defined in spec/authentication_helper.rb):
require 'rails_helper'
RSpec.describe "ResourceName", type: :request do
let!(:user) { FactoryBot.create(:user) }
before { sign_in user }
it "allows authenticated user to access" do
get resource_path
expect(response).to have_http_status(:success)
end
end
Test Naming Conventions
Describe Blocks
- First level: Feature/method name (
describe "#method_name"ordescribe "GET /path") - Second level: Condition (
describe "when user is authenticated") - Use
describefor grouping, notcontext
It Blocks
- Start with a verb describing the behavior
- Be specific about what's being tested
- Avoid "should" in descriptions (implicit in RSpec)
# Good
it "returns true if the user has a pending membership request"
it "creates the project and associated tags"
it "handles extra spaces and normalizes tags"
# Avoid
it "should return true"
it "works correctly"
Expectation Patterns
Testing Changes
# Single change
expect { action }.to change { Model.count }.by(1)
# Multiple changes with .and
expect {
post user_projects_path(user), params: { project: attributes }
}.to change { Project.count }.by(1)
.and change { Tag.count }.by(2)
# No change
expect { action }.not_to change { Model.count }
Testing Values
# Equality
expect(model.field).to eq(expected_value)
# Boolean checks
expect(project.collaborator?(user)).to be true
expect(model).to be_valid
expect(model).not_to be_valid
# Array matching (order independent)
expect(project.tags.pluck(:name)).to match_array(["rails", "saas"])
# Inclusion
expect(results).to include(record)
expect(results).not_to include(other_record)
expect(model.errors[:field]).to include("error message")
# Empty checks
expect(collection).to be_empty
# Response checks
expect(response).to have_http_status(:success)
expect(response.body).to include("Expected Text")
expect(response).to redirect_to(path)
Testing State Changes
it "marks the request as accepted" do
expect {
post accept_path(request)
}.to change {
request.reload.status
}.from("pending").to("accepted")
end
Project-Specific Patterns
Testing Associations
describe "associations" do
let(:model) { FactoryBot.create(:model) }
it "has many associated_models" do
expect(model).to respond_to(:associated_models)
end
it "has many others through join_table" do
expect(model).to respond_to(:others)
end
end
Testing Project Membership/Roles
Always create owner membership explicitly:
let!(:owner) { FactoryBot.create(:user) }
let!(:project) { FactoryBot.create(:project) }
let!(:owner_membership) {
FactoryBot.create(
:project_membership,
user: owner,
project: project,
role: ProjectMembership::OWNER,
status: ProjectMembership::ACTIVE
)
}
let!(:member) { FactoryBot.create(:user) }
let!(:member_membership) {
FactoryBot.create(
:project_membership,
user: member,
project: project,
role: ProjectMembership::MEMBER,
status: ProjectMembership::ACTIVE
)
}
Testing Tag Functionality
# Create with tag_list array
let!(:project) { FactoryBot.create(:project, tag_list: ["rails", "saas"]) }
# Create with tag_names string
let!(:project) { FactoryBot.create(:project, tag_names: "rails, saas") }
# Test tag normalization
let(:project) { FactoryBot.create(:project, tag_list: ["Rails", "SAAS"]) }
it "normalizes to lowercase" do
expect(project.tags.pluck(:name)).to match_array(["rails", "saas"])
end
Testing Scopes with Tags
describe ".with_any_tags" do
let!(:rails_project) { FactoryBot.create(:project, tag_list: ["rails", "backend"]) }
let!(:js_project) { FactoryBot.create(:project, tag_list: ["javascript", "frontend"]) }
describe "when filtering by a single tag" do
it "returns projects that have that tag" do
results = Project.with_any_tags(["rails"])
expect(results).to include(rails_project)
expect(results).not_to include(js_project)
end
end
end
Testing Request Params with Tags
describe "POST /users/:user_id/projects" do
let!(:user) { FactoryBot.create(:user) }
before { sign_in user }
it "creates the project and associated tags" do
expect {
post user_projects_path(user), params: {
project: {
name: "Test Project",
description: "A" * 50,
tag_names: "rails, saas"
}
}
}.to change { Project.count }.by(1)
.and change { Tag.count }.by(2)
project = Project.last
expect(project.tags.pluck(:name)).to match_array(["rails", "saas"])
end
end
Running Tests
# All tests
bundle exec rspec
# Specific file
bundle exec rspec spec/models/project_spec.rb
# Specific line
bundle exec rspec spec/models/project_spec.rb:10
# By type
bundle exec rspec spec/models/
bundle exec rspec spec/requests/
Test Writing Checklist
When writing tests for a new feature:
-
Model tests (if model changes):
- Validations
- Associations
- Callbacks
- Scopes
- Public methods
- Edge cases
-
Request tests (for controller actions):
- Happy path (authenticated user)
- Authorization (different user roles)
- Invalid parameters
- Edge cases
- Response format (status, redirects, content)
-
General:
- Use appropriate
letvslet! - Test both positive and negative cases
- Use descriptive test names
- Group related tests with
describeblocks - Reload records when testing state changes
- Clean up test data (handled automatically by transactional fixtures)
- Use appropriate
Common Patterns Reference
See examples.md for comprehensive test examples from the codebase.
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon