Testing best practices: behavior over implementation details


Tell, Don’t Ask

The principle of “Tell, Don’t Ask” is about shifting from querying objects about their state to commanding them what to do, you can keep your tests clean and focused on behavior rather than implementation details.


Code Examples

Active Record Save Interaction

Consider the typical Rails pattern when working with models:

# app/controllers/articles_controller.rb
if @article.save
  redirect_to @article
else
  render action: 'new'
end

To test this, one might only need to stub the save method:

# test/controllers/articles_controller_test.rb
@article.stubs(:save).returns(true)

Here, #save acts as a command.

It tries to save an Article, handling both validation and persistence, encapsulating everything into a single action that returns a boolean.

By contrast, decomposing this into #valid? and #save! complicates interactions significantly:

# app/controllers/articles_controller.rb
if @article.valid?
  @article.save!
  redirect_to @article
else
  render action: 'new'
end

Such implementation invites multiple stubs for testing:

# test/controllers/articles_controller_test.rb
@article.stubs(:valid?).returns(true)
@article.stubs(:save!)

Named Scopes in Controller

Named scopes can simplify complex queries but may increase coupling between models and controllers:

# app/models/article.rb
class Article < ActiveRecord::Base
  MIN_VOTES_TO_DISPLAY = 4
  FEATURED_CUTOFF_AGE = 7.days

  scope :with_at_least_one_comment, -> { where('articles.comments_count > 0') }
  scope :with_votes_at_least, -> (minimum) { where(['articles.votes_count >= ?', minimum]) }
  scope :created_after, -> (cutoff_date) { where(['articles.created_at > ?', cutoff_date]) }
end

Application:

# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def index
    @featured_articles = Article.with_at_least_one_comment
      .with_votes_at_least(Article::MIN_VOTES_TO_DISPLAY)
      .created_after(Article::FEATURED_CUTOFF_AGE.ago)
  end
end

Testing this involves multiple stubs and mocks:

# test/controllers/articles_controller_test.rb
context "given featured articles" do
  setup do
    chain = stub('chain')
    @featured_articles = [Factory(:article), Factory(:article)]

    Article.stubs(:with_at_least_one_comment).returns(chain)
    chain.stubs(:with_votes_at_least).with(Article::MIN_VOTES_TO_DISPLAY).returns(chain)
    chain.stubs(:created_after).with(Article::FEATURED_CUTOFF_AGE.ago).returns(@featured_articles)
  end
end

Refactor further the model to encapsulate the logic:

# app/models/article.rb
class Article < ActiveRecord::Base
  def self.featured
    with_at_least_one_comment.with_votes_at_least(MIN_VOTES_TO_DISPLAY)
      .created_after(FEATURED_CUTOFF_AGE.ago)
  end
end

And simplify the controller as well as its tests:

# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def index
    @featured_articles = Article.featured
  end
end
# test/controllers/articles_controller_test.rb
context "given featured articles" do
  setup do
    @featured_articles = [Factory(:article), Factory(:article)]
    Article.stubs(:featured).returns(@featured_articles)
  end
end

By focusing on telling models what to do and retrieving the needed information, Rails applications can enhance both test simplicity and robustness.


Related posts

Free Training By Email:
Building a Rails Engine

We will send udpates but we care about your data.
No spams.

Back to homepage