Rspec and BDD

Rspec and BDD

March 1, 2024
development
ruby, rspec, bdd

Context #

In my latest job, after several years of working with Ruby on Rails, I had the opportunity to switch to a Python project for a few months. It was my first substantial and professional experience with Python, and one of the initial distinctions I encountered was in the testing framework. After working with RSpec for a while, I suddenly found myself face-to-face with Pytest.

Initially, it felt unfamiliar, and I struggled to conceptualize and organize my tests. During a meeting where I was discussing this with my colleagues Python developers, one of them, who had never worked with Ruby or RSpec, wanted to understand more and asked, “It’s because RSpec is more focused on BDD, right?” Although the simple answer was “yes” (as it calls itself “Rspec: Behaviour Driven Development for Ruby”), this question made me realize that despite being familiar with both BDD concept and RSpec, I lacked a profound understanding of what BDD truly means, and consequently, I wasn’t really able to discuss the relationship between these two concepts.

I’d like to say that my intention is not to directly compare Pytest and RSpec, as my experience with Pytest is very limited. Instead, I want to provide in this text some clarification, both for myself and anyone else curious, about the relationship between RSpec and BDD.

What’s BDD? #

After some research, here is my understanding of Behavior-Driven Development: BDD is a software development process that has a strong emphasis on the behavioral aspects of software from the end user’s perspective. At its core, BDD encourages collaborative efforts between developers, quality assurance professionals, and business stakeholders to define and comprehend requirements in a language that is both human-readable and technically actionable.

Given When Then #

One of the strategies used for this is the “Given When Then” formula. This is a template intended to describe the behavior of a system or a feature and help in defining and organizing test scenarios.

  • Given: the context, the initial state, or preconditions of the system
  • When: the action or event that triggers the behavior
  • Then: the changes or behavior you expect due to action

Example:

Given a user visits the registration page
When they fill out the registration form with valid information
Then a new user account should be created
And (then) they should be redirected to the home page

This structure helps in creating clear, readable, and executable specifications for softwares.

BDD vs TDD #

This title can be questioned since these two concepts can be seen as complementary rather than opposing, however, there are some differences in approach when writing the tests

Quoting Sandi Metz in Practical Object-Oriented Design in Ruby: An Agile Primer:

Both styles (BDD and TDD) create code by writing tests first. BDD takes an outside-in approach, creating objects at the boundary of an application and working its way inward, mocking as necessary to supply as-yet-unwritten objects. TDD takes an inside-out approach, usually starting with tests of domain objects and then reusing these newly created domain objects in the tests of adjacent layers of code

BDD vs TDD

(If TDD it’s unfamiliar to you, here’s a good and simple explanation)

Finally… Rspec and BDD #

If you’ve had some experience with RSpec, you might already grasp its connection to BDD. My conclusion is that there are two key aspects that ease the adoption of Behavior-Driven Development with RSpec:

1. Human-readable Language #

For illustrative purposes, here’s an example with Pytest:

def test_something():
    pass

def test_one_thing():
    pass

def test_another_thing():
    pass

And now with RSpec:

RSpec.describe "something" do
  context "in one context" do
    it "does one thing" do
    end
  end

  context "in another context" do
    it "does another thing" do
    end
  end
end

Do you notice how effortlessly RSpec enables you to describe your test scenarios? It allows you to organize your specs with precision. The describe, context, and it structures (more about it here) significantly enhance the organization of your test cases. Moreover, they facilitate the creation of highly human-readable descriptions of your code’s expected behavior, aligning perfectly with BDD principles.

If we take the previous “Given When Then” example, it’s easy to fit it into the RSpec structure:

RSpec.describe "User Registration Page" do
  context "when user fills out the registration form with valid information" do
    it "creates a new user account" do
    end

    it "redirects to the home page" do
    end
  end
end

2. Mocks #

Ultimately, although I find the first point to be the most significant, I also want to mention something about mocks. When you initiate your tests from the outside in (as BDD suggests) certain structures needed by the outer layers might not have been implemented at the time of test writing. Unlike other frameworks that are not particularly good at this (hi Jest! 👋), RSpec comes equipped with a robust native library for mocks. Creating mocks, stubs, and setting message expectations for most scenarios is delightful and very straightforward.

Here is a simple example:

RSpec.describe UserService do
  # Create a User double
  let(:user_model) { double('User') }

  let(:user_service) { UserService.new(user_model) }

  describe '#register_user' do
    it 'creates a new user with active status' do
      # Stub the create method of the User model
      expect(user_model).to receive(:create).with(
        username: 'john_doe',
        email: 'john@example.com',
        status: 'active'
      ).and_return(true)

      # Call the method to be tested
      user_service.register_user('john_doe', 'john@example.com')
    end
  end
end

You can read understand about RSpec Mocks here.

References #

Some references that I used to write this post: