DEV Community

Szymon Lipka
Szymon Lipka

Posted on

Let's expect change and reload!

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

What happens when we run the spec?

Passing spec

Wtf

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


Enter fullscreen mode Exit fullscreen mode

Failing spec

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

What happens if I run the test?

Passing spec

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


Enter fullscreen mode Exit fullscreen mode

Failing spec

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


Enter fullscreen mode Exit fullscreen mode

Passing specs

Victory

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)

Collapse
 
katafrakt profile image
Paweł Świątkowski • Edited

Is the service implemented correctly? Obviously not! It will not save changes to the database that are done, which is expected!

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 ;)

Collapse
 
szymonlipka profile image
Szymon Lipka

Makes sense! In this particular case we expect it to persist, so I have rephrased it. Thanks!