Quadragonal
Tags: ruby, rails
Published: 2022-03-23 13:51:31

Bob: My tests take so long!

Steve: Why do they take so long?

Bob: My records take forever to create and save to the database!

Steve: Why does your records taking forever affect your test run so much?

Bob: Because my logic is pulling and storing stuff in the database and I need to test it.

Steve: Why is your logic pulling and storing in the database?

Bob: uhhh... because that is how I learned how to write Rails? You aren't about to lecture me about Hexagonal are you?

Steve: No, but have you ever seen a fast test suite with the way people normally write Rails?

Bob: No, but I feel like you are about go on about something, so go ahead and get it over with.

Steve: So, the key to a fast test suite is to separate handling of logic from handling of state as much as possible. That is fairly easy in most cases. First, you extract the data from the database. Then, you run the logic on the data. Finally, you save the result to the database in a transaction. What does that get you?

Bob: I think I see, you don't have costly database dependencies within the business logic.

Steve: Yes, and the tests for the full unit only needs a success and failure case, because none of it matters unless the transaction runs and succeeds.

Bob: Thats all nice, but ActiveRecord has so many methods that can query the database, it is easy to get tripped up.

Steve: Yes, that is a tricky one. You may want to do something like disabling the connection. This for example prevents calling the database by switching to a non-existing connection specification.

```ruby
class ApplicationRecord
  def self.with_no_connection!(&block)
    old_name = connection_specification_name
    self.connection_specification_name = 'with_no_connection active'
    yield
    self.connection_specification_name = old_name
  end
end
```

Bob: I could perhaps use that during testing but not in production so that I don't get 500s from test concerns.

```ruby
class ApplicationRecord
  def self.with_no_connection(&block)
    if Rails.env.test?
      with_no_connection!(&block)
    else
      block.call
    end
  end
end
```

Steve: Good call.

Bob: To go over it one more time. We improve our testing speed by avoiding calls to the database within the logic. First extract all the needed data, then run the logic on the data, and finally save back to the database. Do you have a little example?

Steve: That sounds right. Here is an example in a controller: 

```ruby
class MovesController < ApplicationController
  def create
    # query db, build objects
    person = Person.include(:places).find(params[:id])
    old_place = person.current_place
    new_place = Place.find_by(params[:new_address])

    # pass objects to stateless logic
    ApplicationRecord.with_no_connection {
      Move.call(person, old_place, new_place)
    }

    # save state in transaction
    transaction do
      person.save!
      old_place.save!
      new_place.save!
    end

    head :no_content
  end
end
```

Bob: That looks pretty close to normal Rails, plus in the Move tests I do not need anything in the database! Interesting. What is with the title of this article anyways?

Steve: Quadragonal is a play on Hexagonal, but there are only 4 parts. The controller, the state, the logic, and the view.