It's often that we create a service that is supposed to change an attribute on an ActiveRecord object. Testing such a service is tricky at times.
Expect change
Let's say we have an incorrect service that is supposed to update
a name of a user in a database, which is looking like this:
class UpdateUserName
def initialize(user)
@user = user
end
def call
# TODO: Will be done in close future, until 2200
end
private
attr_reader :user
end
And we have a spec that is supposed to test it:
require 'rails_helper'
RSpec.describe UpdateUserName do
it "should update user name" do
user = create(:user)
described_class.new(user).call
expect(user.name).to eq "funny name"
end
end
What happens when we run the spec?
Why has it passed you shall ask. The name of the user was always "funny name" although the call of service did not change anything, the spec passed.
We shall rewrite it to something like this:
require 'rails_helper'
RSpec.describe UpdateUserName do
it "should update user name" do
user = create(:user)
expect { described_class.new(user).call }.to change(user, :name).to "funny name"
end
end
Awesome! We've discovered an incorrect implementation with a spec!
Reload an object
Going forward with the same example of spec:
require 'rails_helper'
RSpec.describe UpdateUserName do
it "should update user name" do
user = create(:user, name: "not so funny name")
expect { described_class.new(user).call }.to change(user, :name).to "funny name"
end
end
but we have upgraded our service and it is looking like this:
class UpdateUserName
def initialize(user)
@user = user
end
def call
user.name = "funny name"
end
private
attr_reader :user
end
What happens if I run the test?
Is the service implemented correctly? In this particular case, we expect it to persist changes to database, so it is not correct, but spec is passing.
Let me fix the spec:
require 'rails_helper'
RSpec.describe UpdateUserName do
it "should update user name" do
user = create(:user, name: "not so funny name")
expect { described_class.new(user).call }.to change { user.reload.name }.to "funny name"
end
end
So now we've discovered incorrect implementation again! Let me fix the service:
class UpdateUserName
def initialize(user)
@user = user
end
def call
user.name = "funny name"
user.save
end
private
attr_reader :user
end
Conclusion
Usually, when we want to test services, we should expect a change of an attribute and reload it to see if the change really happened or maybe it is only on the object passed.
Top comments (2)
I don't know, I wouldn't say that it's that obvious. The service name just hints that it updates the user's name, it does not say anything about persisting the change in the database. I can easily imagine a service that updates a shopping cart's total price using a set of complex business rules and returns a dirty AR object with which you can do whatever you want, including persisting the change or piping it to another service that calculates expected delivery date.
If the service does not talk to the database, it's also easier to tests, as we see.
So while probably most services of this kind would actually persist the change in the database, it's the convention of a particular codebase whether it's "obvious" or not ;)
Makes sense! In this particular case we expect it to persist, so I have rephrased it. Thanks!