Let’s say you have such a class: (this code is borrowed from this Reddit thread

class CreateSomethingService
  def initialize(params)
    parse_parameters params
  end

  def run
    Something.create(name: @name)
  end

  private

  def parse_parameters(params)
    @name = params[:name]
  end
end

How can we test this class without loading Rails?

One way to unit test it is by using the repository object and the concept of replacing the repo object with an in-memory one in tests.

class CreateSomethingService
  def initialize(repo, params)
    @repo = repo
    parse_parameters params
  end

  def run
    repo.create_something(@name)
  end

  private

  def parse_parameters(params)
    @name = params[:name]
  end
end

class SomethingsRepo
  def create_something(name)
    Something.create(name: @name)
  end
end

class InMemorySomethingsRepo
  attr_accessor :somethings

  def initialize
    @somethings = []
  end

  def create_something(name)
    @somethings << name
  end
end

class SomethingsTest
  def test_creates_somethings
    repo = InMemorySomethingsRepo.new
    CreateSomethingService.new(repo, "Arkency")
    assert_equal(1, repo.somethings.length)
  end
end

Note that the service now takes the repo as the argument. It means the controller needs to pass the right repo in the production code and we use the InMemory one in tests. Obviously, if your implementations of the repos diverge, you have a problem :) (which best to mitigate by having integration tests which do run this code with Rails)

You can read more about the setup here:

InMemory fake adapters

Rails and adapter objects - different implementations in production and tests

It’s worth noting here, that it may be better to treat a bigger thing as a unit than a single service object. For example you may want to consider testing CreateSomethingService together with GetAllSomethings, which makes the code even simpler, as the InMemory implementation doesn’t need to have the :somethings attribute.

Unit tests vs class tests

Services - what they are and why we need them This setup has its limitations (the risk of diverging), but it’s solvable. The benefit here is that you don’t rely on Rails in tests, which makes them faster.

If you like this kind of approaches to Rails apps, then you will enjoy more such techniques in my book about Refactoring Rails