Testing controllers with a non-trivial database

Hi,

I’m trying to write Minitest tests for my controllers but I’m not sure what is the correct way (if there’s one) of setting up the database. I wrote a simple test including the following lines:

let(:user_repo) { UserRepository.new }
before do
  user_repo.clear
  user_repo.create(id: 1, email: 'user@site', name: 'Name')
end

So far so good, except that a few lines below, I’m also using a related table (understand "has a foreign key to users"):

let(:owns_repo) { OwnsRepository.new }
before do
  owns_repo.clear
  owns_repo.create(car_id: 1, user_id: 1)
end

This fails on the second run with a constraint violation at repository.clear because the values are persisted in the database between the test runs, and because I am not using an ON DELETE CASCADE clause or allowing NULL for my foreign key constraint (it’s part of a composite primary key, so that’s not an option).

So, starting from this small example, I’m trying to figure out what would be the best way of testing my controllers? I can see different ways:

  • Clear all the relevant tables at the top of each unit test, assuming that each unit test is ran in an isolated transaction so that they won’t disturb each other (of which I have no idea whether it is done or not)
  • Populate the testing database once with all needed cases, then run each unit test in an isolated transaction that is ROLLBACKed automatically at the end of the test (that’s how I’m doing it for another project of mine)
  • Use mockups of Repository objects so that they don’t actually modify the testing database, but I have no idea whether it is possible, let alone how to do it

Can you please point me in the right direction?

Two days later, I made some progress. I’m going to document it a bit in here (sorry if it’s turning into some sort of blog, I just hope it’ll help other people one day). The important question is in bold text.

I think that I was initially misled by the starting guide, which needs to hide some complexity at the expense of correctness (in order to remain short and focused). But then I realised that it was detailed in the Actions Testing guide, Dependency Injection section. So I decided to go this way, by using stubs of Repository objects.

Dependency injection

Still, I’m facing some problems. First, adapting the example to Minitest is a bit tricky, because it only has the UserRepository.new.stub(:find) mechanism which is discouraged by the guide (and which turns into a mess of nested things when several Repositories need to be stubbed). It is apparently possible to just pull rspec-mocks and use the double object in Minitest, but I decided to roll out my own implementation instead, just for the fun of tinkering with it. For the record, this is what I added in my spec/spec_helper.rb:

class MyDouble
  def initialize(hash)
    call = caller_locations.first
    @name = "<#{call.path.split('/')[-1]}:#{call.lineno}>"

    hash.each { |k, v| define_singleton_method(k) { |*a| v } }
  end

  def to_s; @name; end
  def inspect; to_s; end
end

The @name variable stores the location from which the stub was created, so when Ruby raises a NoMethodError during a test, it is easy to know which stub is missing the method (very useful with nested describe clauses). Then singleton methods are created for each pair of key/value found in the Hash, similar to what RSpec’s double does. I use it this way:

  let(:user) { User.new(id: 1, email: 'user@site', name: 'User') }
  let(:user_repo) { MyDouble.new(find: user) }

But now, the consequence is that, for each controller, I need to write additional code for dependency injection, and it feels quite repetitive, and we don’t like repetitive tasks, do we? So, would it make sense for Hanami to automatically insert a dependency injection mechanism at controller creation time?

The session object

How to create the session object turned out to be so simple, once I found the solution hiding somewhere. For the record, it can be done like this:

  let(:session) { { user_id: 1 } }
  let(:params)  { { 'rack.session' => session } }

That’s all I need for my authentication mechanism to validate the calls, your mileage may vary. Still, there is something bothering me because I need to copy and paste too many of these let statements from one spec file to another. There should be a way to define these variables as top-level (I’m thinking Minitest::Spec level) so that they can be overridden when necessary in the spec files. This is not a question about Hanami itself, I know.

Thank you for your attention and patience if you went through all this beginner’s story!

1 Like