---
title: "Write a suitable RSpec test"
date: 2020-02-20T00:00:00+08:00
publishDate: 2020-02-20T00:26:16+08:00
lastmod: 2025-10-19T16:30:03+08:00
tags: ["Ruby","RSpec","Experience","BDD","TDD","Rails"]
toc: true
permalink: "https://blog.aotoki.me/en/posts/2020/02/20/Write-a-suitable-RSpec-test/"
language: "en"
---


Include me, write test is many people's nightmare. Many junior programmers feel it is hard to define which should be tested. So, I decided to share my experience after I tech my colleague today.

<!--more-->

Before you start talking about how to write a test, you can stop thinking about anything about TDD or BDD or any you may read about it.

And ask yourself, what is "test?" Why you need a "test?"

The target you add the tests to your project usually to prevent human mistake, that means you try to let compute help you to confirm your code matches the "spec."

But you have to know, the test still is human write code and the spec is human to decide. When you have the wrong spec and wrong way to test, you still get the wrong result.

So, you can try to keep everything simple, and you will feel happy when writing the test.

## The Pure Ruby example

In my experience, the test is related to your code. If you have bad code, and you will hard to test it. So, no matter you write the test before implementing anything or after it. The most important thing is the double-check which you want to test and fit your necessities.

You can write a `Calculator` class, and try to test it.

```ruby
class Calculator
  def initialize
    @inputs = []
  end
end
```

At first, you have a `Calculator` class with initialized `@inputs` array.

And you can create a simple RSpec skeleton.

```ruby
RSpec.describe Calculator do
  let(:calculator) { Calculator.new }
end
```

And next, you can add the `#add` method to the calculator to allow it to add something.

```ruby
class Calculator
  def initialize
    @inputs = []
  end

  def add(number)
    @inputs << number
  end

  def perform
    @inputs.sum
  end
end
```

And you can update the test

```ruby
RSpec.describe Calculator do
  let(:calculator) { Calculator.new }

  describe '#add' do
    let(:number) { 1 }
    subject { calculator.add(number) }

    it { is_expected.to include(number) }
  end

  describe '#perform' do
    subject { calculator.perform }

    before { calculator.add(1) }

    it { is_expected.to eq(1) }
  end
end
```

In my experience, the best case is you can simply define a `subject` which is the target you want to test for, and you can use one line to test it. So I usually try to let my code can be tested like the above example.

> In the real world, it may not usually ideal. But this post didn't discuss these case, maybe it can be discussed in the future.

## Real-world example

After you have an imagination about the ideal test, you can try to apply it in the real world.

This morning I am discussing a legacy object which is the order's payment processor with my team member.

```ruby
class PaymentService
  def initialize(payment)
    @order = payment.order
    @payment = payment
     # ...
    setup
  end

  def setup
    @payment.amount = amount
    @payment.currency = @order.currency
    # ...
  end

  def perform
    return false unless @payment.valid?

    ActiveRecord::Base.transaction do
       @payment.save
       VendorAPI.payment.create(amount: @payment.amount)
       # ...
    end
  end

  private
  def amount
    @order.items.sum(&:subtotal)
  end
end
```

When you try to test this class, you notice it is very hard to add any test for it. Because the information is encapsulation inside the `@payment` but you cannot access it.

You may want to expose the `@payment` as an attribute like `service.payment.amount`

But if you try to check for the amount is correct, the test code does not make sense.

```ruby
subject { service.payment.amount }
it { is_expected.to eq(100) }
```

You test for the "Service Object" not the "Payment Model" inside it. According to this rule, the test should be like below.

```ruby
subject { service.amount }
it { is_expected.to eq(100) }
```

At this moment, the "subject" correctly refers to the service's amount.

You can refactor the `PaymentService` class to fit the expectations.

```ruby
class PaymentService
  def initialize(order)
    @order = order
  end

  def amount
    @order.items.sum(&:subtotal)
  end

  def perform
    payment = build_payment
    return false unless payment.valid?

    ActiveRecord::Base.transaction do
      payment.save
      VendorAPI.payment.create(amount: amount)
      # ...
    end
  end

  private

  def build_payment
    @order.payments.build(
      amount: amount,
      currency: @order.currency
    )
  end
end
```

After refactoring, the `PaymentService` is becoming more straight and you can focus tests on the `PaymentService`.

This is my experience when I design an object and I usually follow this rule in my work.

## More example of Rails

The Rails is the popular framework in Rubyist, I use it almost every workday. How can we use the above skills in Rails?

Just keep your class simple, and everything will be easier to test.

```ruby
# Model
RSpec.describe User do
  it { should validate_presence_of(:email) }
  # ...

  describe "#avatar_url" do
    let(:email) { "example@example.com" }
    let(:user) { create(:user, email: email) }
    subject { user.avatar_url }

    it "returns Gravatar URL" do
       digest = OpenSSL::Digest::MD5.hexdigest(email)
       should eq("https://www.gravatar.com/avatar/#{hash}")
    end
  end
end
```

For the model, I usually prevent logic inside it. If your project is small and simple, it is ok to do this. But when your project is complex, you usually have to take several steps to process one thing. And that may be a signal to you to split it into an independent class to focus on this process (commonly called service object)

```ruby
# Request
RSpec.describe "/api/users", type: :request do
   describe "GET /api/users" do
     let(:users) { create_list(:user, 5) }
     before { get api_users_path }
     subject { response.code }
     it { should eq("200") }

     describe "body" do
       subject { JSON.parse(response.body) }
       it { should_not be_empty }
       # ...
     end
   end
end
```

If it is possible, I usually try to make my test more simple. It will help you to think about how to design the class is more clear and easier to use.

The above examples only cover very small parts of tests, but I think it is enough to show the suitable test usually depend on your code.

I still not used to write the test before I start work, and I also skip some tests if I have no time to write it.

But according to my experience, even you didn't write the test you still need to think about "when you try to test your code, which is easier?"

And then, you will notice the best practice you read from the net if you follow it and usually let your code easier to be tested.

For example, the junior will define a method mix different type return values.

```ruby
def sum
  return false if summable?

  @items.sum
end
```

It will cause it hard to predict which type will be returned, and you need to write more test cases to confirm it.

## Conclusion

This may not an advanced skill, but I spend a lot of years to learn and try to write a test suitable.

And I notice my company's junior also has the same problem and feeling confusing when I ask them to try to refactor some legacy code.

They feel lost their way and didn't know where they can start to refactor the code.

So, when you feel confusing, just check for your code about:

* Is the test can focus on my class without depending on others?
* Is my behavior is focused on one thing? (ex. Read and write, validate value, send an API request)
* Is my method returns is expectable? (ex. the only number, object have the same interface)

That sounds very simple and you may read about some object-oriented article about SOLID rules. But it still is hard to design it to a suitable state which didn't have too many over design.

Anyway, hope my article can help you find some inspiration when you try to write some tests.

