DEV Community

Cover image for Structured RSpec For Ruby & Rails
Edwin Mak
Edwin Mak

Posted on • Edited on

Structured RSpec For Ruby & Rails

Introduction

In my earlier years of software development, I paid little attention to the practice of building tests for my production applications. Of course my blood pressure, stress, and anxiety levels reached new peaks during the deployment of new code. And my reasons for not testing are I believe common amongst the community. It is thought that "writing tests take too long". I have realized that this feeling could be caused by the absence of structure and an underlying method that drives test/spec creation.

In this article, I share my thought process and methods (/w examples) for creating specs/test for my Ruby & Ruby On Rails projects. My methods are highly influenced by my previous mentors and Better Specs. I hope that you (the reader) after reading this article will be empowered to add test/spec creation in their everyday workflow.

Let's feel good when we deploy new code :)

What are you going to describe and what is your subject?

Without using subject or describe it can be hard to know exactly what the author of the spec was intending. I utilize describe in specs to keep track of what part of my code/method I am testing and subject to define it. Here is an example of a spec that describes some Animal object and a instance method called #speak:

describe AnimalSpec do
  describe '#speak' do
    # Any call to `subject` will execute & return the output of the code block.
    # We now know that the author intends to test the output of #speak on varying
    # instances of `Animal` 
    subject { Animal.new.speak }

    it "should return an animal noise" do
      # The code beneath then does something more like: 
      # expect(Animal.new.speak).to eq('an animal noise')
      expect(subject).to eq('an animal noise')
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

This keeps things tidy and more importantly focused. That is, we are testing here the output of varying instances of Animal. Although this test isn't that interesting yet. Let's utilize let in the next section to enable us to match varying context and ultimately test the various outputs of your subject

Let's use let to setup the context

In order to expand our specs to cover varying instances of Animal and their corresponding output for #speak, we will utilize let and context. The let method allows us to modify the output value of methods. And the context is made to describe in written words what situation the subject is in. Here is an example with the same Animal spec but this time add variance of the output by introducing the animal_type and angry parameter:

describe AnimalSpec do
  describe '#speak' do
    subject { Animal.new(animal_type: animal_type, angry: angry).speak }

    context "when the animal_type is 'duck'" do
      # We utilize `let` to change the inputs of our subject to match
      # the situation we are describing in our context.
      let(:animal_type) { 'duck' }

      context "and the animal is not angry" do
        # Moreover, you can utilize `let` to continue to describe the
        # context further. 
        let(:angry) { false } 

        it "should return 'quack'" do
          expect(subject).to eq('quack')
        end
      end

      context "and the animal is angry" do
        let(:angry) { true }

        it "should return 'QUACK!'" do
          expect(subject).to eq('QUACK!')
        end
      end
    end

    context "when the animal_type is 'dog'" do
      let(:animal_type) { 'dog' }

      context "and the animal is not angry" do
        let(:angry) { false } 

        it "should return 'woof'" do
          expect(subject).to eq('woof')
        end
      end

      context "and the animal is angry" do
        let(:angry) { true }

        it "should return 'WOOF!'" do
          expect(subject).to eq('WOOF!')
        end
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

By using let and context we can more clearly display our situations and varying characteristics of our Animal instances.

Applying it to Ruby On Rails projects

Now that we've covered the basics for building specs for plain ruby code. Let's now dive a little deeper by putting this into practice for some common situations you may encounter when working with Rails.

Model Validation

Let's say you have a User database model that requires a valid unique email and a password. We can first create a describe block that says "validations" and a subject the returns the #errors after performing #valid?. In this approach, I define in the outermost context the valid attributes for a User and with let modify them into a invalid state which we can test generates an expected error:


describe User do
  describe 'validations' do
    # I want to execute the `#valid?` operation to populate the errors and
    # then the errors for the following conditions. 
    subject { user.valid?; user.errors }

    let(:user) { User.new(user_attrs) }

    # Note - `let` (and `subject`) also accepts code blocks
    let(:user_attrs) do
      {
        email: email,
        password: password
      }
    end
    let(:email) { 'fake_valid_email@example.com' }
    let(:password) { 'fake_password' }

    context 'when the user_attributes are valid' do
      # No need to use `let` here as the outermost is already defining 
      # the valid attributes.
      it 'should have no errors' do
        expect(subject).to be_empty
      end        
    end

    context 'when the email is nil' do
      # This overwrites the value in the outer context.
      let(:email) { nil }

      # Be descriptive about what your are expecting to be returned
      # by your `subject`
      it "should have an error that says email can't be blank" do
        # `subject` is the errors hash as defined above. So we can 
        # dig in and see what errors are included.
        expect(subject[:email]).to include("can't be blank")
      end
    end

    context 'when the email is in an invalid format' do
      let(:email) { 'not_a_email' }

      it "should have an error that says email is invalid" do
        expect(subject[:email]).to include("is invalid")
      end
    end

    context 'when the email has been taken already' do
      # To trigger the expected uniqueness error we create User prior to calling
      # `subject` with the same email
      before do
        User.create!(email: email, password: password)
        # or also `User.create!(user_attrs)` works
      end

      it 'should have an error that says has already been taken' do
        expect(subject[:email]).to include('has already been taken')
      end
    end

    context 'when the password is nil' do
      let(:password) { nil }

      it "should have a error that says password can't be blank" do
        expect(subject[:password]).to include("can't be blank")
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

This approach gives us a guideline to handling the various error states that a instance of User could be in. These ideas can be expanded to larger concepts or situations by merely changing the subject and the let that configure it into varying states.

Side-Effects Or Queueing Of Jobs

Once in a while you may encounter situations where you don't exactly care about the return value of your subject but rather the side-effects it has. For example, let's say you have a method on User called #reset_password in which you only care that the instance of User now has their password reset via a change event. Here we utilize the -> { code } (lambda) syntax to make it more apparent that we are interested in the effects of executing the code rather than the output:

describe User do
  describe '#reset_password' do
    # The subject is now a lambda which you can trigger via `subject.call`
    # It will become more apparent why we want to do this in the next sections.
    subject { -> { user.reset_password } }

    let(:user) { User.create!(password: original_password) }
    let(:original_password) { 'original_password' }

    it "should change the password (method 1)" do
      # Check that the password is initially set to what you expect
      expect(user.password).to eq(original_password)
      subject.call
      # Use reload to ensure you get the latest copy of the `User` record
      expect(user.reload.password).not_to eq(original_password)
    end

    # Or asserting that the change happened via `change` matcher
    it "should change the password (method 2)" do
      expect { subject.call }.to change(user.reload.password)
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Now let's say we wanted to add some extra functionality on #reset_password that enables us to specify that password reset instructions should be emailed to that user. Let's make that parameter be send_instructions and utilize mocking to check that the job or class responsible for sending the email has been called or not called. Let's call that job PasswordResetSender:

describe User do
  describe '#reset_password' do
    subject { -> { user.reset_password(send_instructions: send_instructions } }

    # Let's mock out the class that would be triggering so we can see that it
    # was called were we expect it to be.
    before do
      allow(PasswordResetSender).to receive(:deliver)
    end

    context "when send_instructions is set to true" do
      let(:send_instructions) { true }

      it "should trigger a sending the password reset instructions" do
        subject.call
        # Check that the class responsible for sending the password instruction 
        # had been sent.
        expect(PasswordResetSender).to have_received(:deliver)
      end
    end

    context "when send_instructions is set to false" do
     let(:send_instructions) { false }

      it "should not trigger a sending the password reset instructions" do
        subject.call
        # Vice a versa, we want to ensure sending the password instruction does
        # not occur.
        expect(PasswordResetSender).not_to have_received(:deliver)
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this article, I have covered my thought process & methodology in applying RSpec to your Ruby or Ruby On Rails project. I hope this has been enlightening and inspiring to add test/spec into your regular workflow.

This is just one resource out of many that provides tips on how to apply RSpec effectively. One of my favorite sources where I drew inspiration for ideas is BetterSpecs.

Feel free to reach out to me if you have any questions or other situations that you want to know how to effectively spec out :).

Top comments (0)