DEV Community

Cover image for Introduction to RSpec
Adriana DiPietro
Adriana DiPietro

Posted on

Introduction to RSpec

I've been using RSpec to test my Ruby code for about 8 months now and have learned a lot. So, in this post, I want to share some of the best practices that I've learned about using RSpec for testing. This content is targeted toward beginners who want to get familiar with RSpec, but also can totally benefit more experienced developers who need a quick refresh.

Whether you are just getting started or not, here are some things that I wish I knew back 8 months ago:


Testing is as important as writing code

Testing is as important as writing code, if not more so. It is one of the aspects of software development that I both underestimated and dreaded early in my career.

Testing allows you to find bugs before your code gets released. Testing also helps with understanding complex systems and finding out where there is room for improvement. For example: what happens when someone enters the wrong data type into a field? How do different inputs affect your output? Does this piece of logic always work as intended? We can answer these questions through testing.

In my personal opinion, I think testing has made me better at coding because in order to write successful RSpec tests, you need to understand your code. There have been times where I have written tests and then realized my code could be refactored to be more efficient or more DRY.


RSpec is very feature-rich

Mocking

In a colloquial sense, to mock something is to replicate. Remember this definition as we proceed with rspec mocks.

Mocking is mirroring or replicating your Ruby code into rspec in order to test it properly. It is extremely helpful in perceiving return values or method implementations. Given an edgecase, does your expected return value stand? Given an edgecase, does your method get called? Given an edgecase, how do two objects interact? Let's look at an example.

Here we have a simple Ruby class with a class method that calls an instance method to update a user's city.

class UpdateUserInfo

  def self.update_city(user:, city:)
   new.update_city(user: user, city: city)
  end

  def update_city(user:, city:)
    user.update!(city: city)
  end
end
Enter fullscreen mode Exit fullscreen mode

Now, here is our rspec file. First, I mock out what my user object should look like in reality.

let(:user) do
  {
    first_name: 'Frances',
    email: 'frances.dev@example.com',
    hobbies: [
      'writing',
      'coding',
      'reading'
    ],
    city: 'New York City'
  }
end

let(:updated_city) { 'Philadelphia' }

describe '.update_city' do
  subject { described_class.update_city(user: user, city: updated_city) } 

  it 'updates the user's city' do
    expect(user).to receive(:update!).with(city: 
    updated_city).and_return(true)
  end
end


Enter fullscreen mode Exit fullscreen mode

I then mock out my call to #update! using an expectation. I include what I would normally include: the attribute name and the value I want to pass in. I also want to mock out the expected return or response. A successful call to #update! on the user object should return true. If it were to fail, #update! would raise an error. In this example, the spec will pass.

You may notice that this doesn't look to different than our Ruby code in the UpdateUserInfo class. That is the whole point: readability and mirroring. Just by solely reading this test, I know what that class should accomplish.

Mocking out both the object and the expected behavior allows for an additional perspective on their interaction. How does this object react to this method? Keep this in mind while building out your spec files — maintain the integrity of your code through replication. If your spec doesn't mirror your code, then your spec is not testing truly.

Matchers

What is a matcher? A matcher allows us to create an assertion and expectation within our spec. This is vague, I know. Let's look at a simple example:

let(:random_num) { 14 }

it { is_expected.to be < 20 }
it { is_expected.to be >  1 }
Enter fullscreen mode Exit fullscreen mode

In this code snippet, I declare a variable random_num and assign it to the value of 14. Then I assert two (2) expectations. I assert that it (meaning our declared variable random_num) should pass two different cases: random_num is less than 20 and random_num is more than 1.

In this example, both tests would pass. So, if the object is the variable I declared and the expectation is the test, then what is the matcher? The matcher is .to be. This is only one of many, many matchers. Let's look at a more complex example:

let(:fruits) { ['orange', 'mango', 'strawberry'] }

expect(fruits).to include('blueberry')
expect(fruits).to include('mango', 'strawberry')
expect(fruits).to include('blueberry', 'mango', 'banana')
Enter fullscreen mode Exit fullscreen mode

After taking a second to look at the above code snippet, can you point out the following?

  1. What is the name of the declared variable?
  2. What is the value of the declared variable?
  3. What data type is the value of the declared variable?
  4. What is the matcher of the expectations?
  5. What tests pass? What tests fail?

Look at Resources to discover more types of rspec matchers.


Abide by formatting rules

Just like programming languages, it is best practice to abide by formatting rules and syntax. If there is a clear, defined standard to your specs, then you will eventually be able to understand others' tests and write yours more efficiently. And then on the flip side, this will help other people understand what your code is to achieve.

Some formatting examples include:

  • Test one thing per example block.
  • Make your shared examples, context blocks and it blocks readable and read-like-english.
  • Like Ruby, use snake_case.
  • One test suite per class.
  • If the class file is 'cat_controller.rb', then the spec file for that class should be 'cat_controller_spec.rb'
  • Keep all your RSpec files in a /spec directory.

These are just a few formatting expectations. However, they are a great jumping off point and will set a sound foundation for your test development.


Practice makes progress

Taking a look at RSpec as a whole, you may be confused at first – I sure was. You may be confused after weeks of writing with RSpec – guilty, again! Just keep practicing — this stuff is not easy! Remember: practice makes things easier for you in the future. Don't give up.

How can you practice? Write your tests first before your code. Think Test Driven Development (TDD). Offer to shadow/pair with your peers on writing their specs. Even better, review your peers' specs and see if and how they can be written better. Offer a solution!


Conclusion

Writing tests is a great way to make sure you are building the right thing – in the right way. RSpec syntax, while tricky at first, is simple and expressive. So once you get used to it, writing in RSpec will feel like second nature.

I hope this post has helped you learn a little bit more about RSpec. As I mentioned at the start, it can be a bit overwhelming at first, but don't let that stop you from working with this awesome tool.

If you have any questions about RSpec or want to share your own experiences with it, please leave a comment.

Remember: we are all learning as we go :) so be kind.


Resources

Top comments (0)