Maybe you or your team have decided to code “in the right way” and go hard on TDD and unit testing on your Rails app… And to do that you started to read about it and discovered that “is better to write your unit tests in isolation” because they need to run fast, and also that “you should write your test first” because you are doing TDD… And although you really want to do it, you then started to feel paralyzed… you don’t know how to start!

Maybe is easier with your feature or system tests, because you don’t have to “fake” anything and also you just take the user story and it “tells you” what to test…

But then when you want to test you models or controllers… What do you do? How do you isolate a model? How do you isolate a controller?… Well, is not that easy… I think I am with DHH in this, is “easier” to test them directly and letting them hit the database…

So then… what is the secret?

Well… The secret for doing Unit Testing on your Rails app is to not do it!… Yes that is the secret… You just test it through integration tests and feature or system tests, and also is not necessary to do it first, although a lot of people actually do it with success, but I don’t really do it and I think you will be fine (in a lot of cases)…

But there is another secret…

You don’t have to test all your code through integration tests. Just the “Rails code”… You can always write code that is a little outside of Rails (like a little class, module) and do unit tests for it, and test it in isolation and also you can do it test first… Actually it could be very helpful for you to do TDD, because it will help you to design the API… And also you can write in this way a great part of your application!!

And the last secret… How do you start writing this code that is a little outside Rails?

I will recommend you to start just with a module and a function!!… And please, please!!… don’t start writing interactors, repositories, service objects, presenters or any of the patterns that you will read or have read about… Those patterns are good but they should not be mandatory, you should use them just when you think you need them…

So, that’s the secret… if you want to do Unit Testing on a Rails app, start with a simple non rails module function.

Hmmm… do you want an example?

Well, here is an example of a function that we have in a real application, to show a list of projects…

module DevProjects
  def self.get_projects(store)
    store.find_all_loading_developer_projects_charges_and_documents.map do |record|
      charges = BuildCharges.(record.charges)
      documents = BuildDocuments.(record.documents)
      status = FillingStatus.new(record, charges, documents)
      DevProject.new(WithDeleteStatus.new(record), status.total)
    end
  end
end

That “store” on the Rails App is an ActiveRecord class that actually talks to the database… but for our tests is just a object. And all the functions and classes inside are also plain old ruby functions and objects of that module…

We consider those internal functions and classes as implementation details, so we try to test all the expected behavior just by calling the function with different states in the store.

And that’s all…

  1. Write a module
  2. Write a function
  3. Put all the special behavior inside…

And that’s it!… You can write isolated unit test for the output of that function… You can start doing it right now =)… This is nothing new, is just that is not that obvious… And the code is not that bad, don’t you think?

Then we use that code in a Rails controller in this way…


class DevProjectsController < ApplicationController
  STORE = DevProject

  def index
    render locals: { projects: DevProjects.get_projects(STORE) }
  end
end

We actually have no test for the controller, but if you think you need to test this code, I think is better if you write an integration tests for this.

So in conclusion…

With this separation you can write unit tests for functions like DevProjects.get_project without much trouble… and write integration tests for your controllers (or other Rails code) if you think is needed =)…