3 ways of testing private methods in Rails

Here’s a topic that pops up often especially when faced with the common task of testing controllers in Rails. We have some logic inside a private method and we want to test it somehow. And of course, like most things in ruby, there are a few ways to do it which we’ll look at next.

Public and private methods

Testing private methods directly

This is the most popular one mostly because it’s quick and it’s also the first one you’ll find by google-ing (or searching on stackoverflow.com). It’s all about using the send method to call the private method directly. So you call it like any other public method and voilà, your test is done.

Let’s see how this looks like in code:

class MyClass
  def self.my_public_method
    my_private_method
  end

  private
    def self.my_private_method
      "private method"
    end
end

So here’s a RSpec test for it:

it "tests the private method directly" do
  expect(MyClass.send(:my_private_method)).to eq("private method")
end

There are mixed opinions about testing private methods like this so I’m gonna lay out the pros and cons. But I have to mention that it’s not my favourite.

Pros:

  • It’s quick and easy to write the tests
  • You can test it once then mock it later

Cons:

Testing private methods through their interface

Another way of testing private methods is through their public interface. This means that you have a public method that makes use of the private one and you’re going to test the private method through it.

So given the same ruby class defined above, here’s how our rspec test looks like when testing it’s public method’s behaviour:

it "tests the private method through it‘s interface" do
  expect(MyClass.my_public_method)).to eq("private method")
end

The nice thing about this method is it respects encapsulation but on the other hand if you’re unlucky enough to work with existing code that’s not really well structured, it’s a bit more work to organize it properly (it could take a fair amount of time, depending on it’s complexity of course).

Pros:

  • It forces you to design your code better
  • It respects BDD (testing the behavior and not the implementation)
  • It makes code and tests easier to manage

Cons:

  • It’s not easy to use on spaghetti code so you might need to spend some time refactoring

Extracting the code out into a different class

Another way to organize your code is to extract the logic from the private method into a separate class (or more) that is specific to one problem. By extracting the logic into a separate class (even if it’s a nested class), it makes it easier to test and read. It’s a good practice to separate logic into smaller objects if you can afford the time.

Let’s see an example:

# /lib/my_class.rb
class MyClass
  def self.my_public_method
    MyOtherClass.my_second_public_method
  end
end
# /lib/my_other_class.rb
class MyOtherClass
  def self.my_second_public_method
    "second method"
  end
end
# /spec/lib/my_class_spec.rb
RSpec.describe MyClass do
  describe ".my_public_method" do
    it "calls MyOtherClass's .my_second_public_method" do
      expect(MyOtherClass).to receive(:my_second_public_method)
      MyClass.my_public_method
    end
  end
end
# /spec/lib/my_other_class_spec.rb
RSpec.describe MyOtherClass do
  describe ".my_second_public_method" do
    it "returns 'second method'" do
      expect(MyOtherClass.my_second_public_method).to eq("second method")
    end
  end
end

Pros:

  • It’s better OOP to organize code into smaller and more focused objects
  • Makes for easier testing
  • Test once, mock later

Cons:

  • It’s more work to extract everything out
  • Some people don’t like having to search through tons of files

Private methods and controllers

We tend to use the private methods a bit more in Rails so I think it’s worth sharing an example of how you would test private methods inside of a Rails controller.

It’s nice to have a finder method for each action defined as a private method so you code is a bit more DRY, but then there’s the question of testing it.

Some prefer to use the direct method and get away with it but for those of us who don’t like using the direct method, there’s another way.

We can test one action’s behavior which will also test the private method indirectly but then what do we do with those other actions that make use of the private method? Do we write the same tests for each one? Of course not.

We can use a shared example that will test the private method’s behaviour thru each of the actions that are using it.

Using a shared example becomes useful when you want to test that a public method “includes” some behaviour that’s common across multiple methods.

Let’s see an example of a controller who’s actions need a finder.

class MyController < ApplicationController
  before_each :find_my_user, :only => [:show, :edit]

  def show
  end

  def edit
  end

  private
    def find_my_user
      @user = User.find_by!(:id => params.require(:id))
    end
end

Testing .find_my_user’s behaviour through both these actions is easy enough using a shared example:

require 'rails_helper'

RSpec.shared_examples "a finder" do |action_name|
  it "returns a string" do
    allow(User).to receive(:find_by!).with(:id => "1").and_return(user = double)
    get action_name, :id => 1
    expect(assigns(:user)).to eq(user)
  end
end

RSpec.describe MyController, :type => :controller do
  describe "GET show" do
    it_should_behave_like "a finder", :show
  end

  describe "GET edit" do
    it_should_behave_like "a finder", :edit
  end
end

Final words

If you need (or like) to start with everything inside a the public method, I’d say make it pass first, even if it’s all in one huge method then extract bits of logic out into private methods or different objects while keeping your tests green.

I would love to hear how you’re solving this issue or if you have a better way that I’ve missed.

If you want to read some more about the subject of testing private methods here are two good articles I’ve found:

If you liked this article, please take a moment and say thanks by sharing it on your favorite social media channel.