Introduction
Model tests verify your business logic, validations, associations, and custom methods work correctly. They are the fastest tests to write and run, and they cover the core logic of your application.
Key Concepts
- Model test: A test class inheriting from
ActiveSupport::TestCasethat tests a model's validations, methods, scopes, and associations. - Validation testing: Verifying that models accept valid data and reject invalid data with appropriate error messages.
- Association testing: Confirming that model relationships (has_many, belongs_to) work correctly.
- Scope testing: Ensuring named scopes return the correct subset of records.
Real World Context
Model tests are the foundation of your test suite. They run in milliseconds, require no browser, and test the logic that matters most. A well-tested model gives you confidence that your business rules are enforced regardless of how the data is accessed (web, API, console).
Deep Dive
Testing Validations
rubyclass Article < ApplicationRecord validates :title, presence: true, length: { minimum: 5 } validates :body, presence: true end
rubyclass ArticleTest < ActiveSupport::TestCase test "valid with all attributes" do article = Article.new(title: "Hello World", body: "Content") assert article.valid? end test "invalid without title" do article = Article.new(body: "Content") assert_not article.valid? assert_includes article.errors[:title], "can't be blank" end test "invalid with short title" do article = Article.new(title: "Hi", body: "Content") assert_not article.valid? end end
Testing Associations
rubyclass ArticleTest < ActiveSupport::TestCase test "has many comments" do article = articles(:published_article) assert_respond_to article, :comments end test "destroys comments when destroyed" do article = articles(:published_article) article.comments.create(body: "Test comment") assert_difference "Comment.count", -1 do article.destroy end end end
Testing Custom Methods
rubyclass Article < ApplicationRecord def published? published_at.present? && published_at <= Time.current end def reading_time (body.split.size / 200.0).ceil end end
rubyclass ArticleTest < ActiveSupport::TestCase test "published? returns true for past dates" do article = Article.new(published_at: 1.day.ago) assert article.published? end test "reading_time calculates correctly" do article = Article.new(body: "word " * 400) assert_equal 2, article.reading_time end end
Testing Scopes
rubyclass ArticleTest < ActiveSupport::TestCase test "published scope returns only published articles" do published = Article.create(title: "Pub", body: "x", published_at: 1.day.ago) draft = Article.create(title: "Draft", body: "x", published_at: nil) results = Article.published assert_includes results, published assert_not_includes results, draft end end
Common Pitfalls
- Only testing the happy path: Always test both valid and invalid cases. Test edge cases and boundary conditions.
- Testing Rails itself: Don't test that
validates :title, presence: trueworks. Test your specific business rules and combinations. - Shared state between tests: Each test should be independent. Use
setupfor shared setup and don't rely on test execution order.
Best Practices
- Test validations by checking both valid and invalid states.
- Test custom methods with various inputs including edge cases.
- Use
assert_differenceto verify that create/destroy operations change record counts.
Summary
- Model tests inherit from
ActiveSupport::TestCaseand live intest/models/. - Test validations by creating invalid objects and checking
errors. - Test associations with
assert_respond_toandassert_differencefor dependent destroy. - Test custom methods with representative inputs and edge cases.
- Model tests are fast and should be the largest portion of your test suite.
Code Examples
ruby
class ArticleTest < ActiveSupport::TestCase
test "invalid without title" do
article = Article.new(body: "Content")
assert_not article.valid?
assert_includes article.errors[:title], "can't be blank"
end
test "destroys comments when destroyed" do
article = articles(:published_article)
article.comments.create(body: "Test")
assert_difference "Comment.count", -1 do
article.destroy
end
end
end