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.
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.
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:
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.
Top comments (0)