Why testing?

Testing is an essential part of the development process, and RSpec is one of the most popular testing frameworks for Rails. In this article, we will go over the basics of using RSpec for testing in a Rails app, and provide examples of how to write efficient and effective tests for the Trip, Destination, and User models, as well as the TripsController.

Setting up RSpec in Rails App

In my previous article I showed how to add RSpec to an existing Rails App and how to start writing tests. Here’s a quick catch up on how to do that.

To get started with RSpec in a Rails app, you’ll need to add the RSpec gem to your Gemfile and run the bundle install command. You’ll also need to run the rails generate rspec:install command to set up the necessary configuration files.

Once you’ve done that, you can start writing tests by creating a new file in the spec directory with a _spec.rb extension. For example, to write tests for the Trip model, you would create a new file called trip_spec.rb in the spec/models directory.

Writing Model Specs

When writing model specs, it’s important to focus on testing the validations, associations, and methods of the model. For example, in the case of the Trip model, you might write tests to ensure that a trip has a start_date and end_date, and that the user association is set up correctly.

Here’s an example of how you might write specs for the Trip model:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
require 'rails_helper'

RSpec.describe Trip, type: :model do
  subject(:trip) { create(:trip, user: user) }
  let(:user) { create(:user) }

  it { should validate_presence_of(:start_date) }
  it { should validate_presence_of(:end_date) }
  it { should belong_to(:user) }
  it { should have_and_belong_to_many(:destinations) }

  describe '#duration' do
    it 'calculates the duration of the trip' do
      expect(trip.duration).to eq(trip.end_date - trip.start_date)
    end
  end
end

In this RSpec test, we are testing the Trip model. The first part of the test sets up two variables: trip and user. The user variable is created using the create method and the :user factory. The trip variable is created using the create method and the :trip factory, and it is passed the user: user argument to associate the trip with the user.

The create method is provided by the factory_bot gem, which helps us on creating instances for our tests.

Next, we have four it blocks that use the should syntax to test for various validations and associations on the Trip model. The first two blocks use the validate_presence_of method to test that the start_date and end_date fields are required. The third block uses the belong_to method to test that the Trip model belongs to the User model. The fourth block uses the have_and_belong_to_many method to test that the Trip model has a many-to-many relationship with the Destination model.

Finally, we have a describe block that tests the duration method on the Trip model. The it block inside the describe block uses the expect method to test that the result of the duration method is equal to the difference between the end_date and start_date fields of the trip.

You may also want to test the Destination model, and to do so, you can use the same approach.

Writing Controller Specs

When writing controller specs, it’s important to focus on testing the behavior of the controller’s actions. For example, in the case of the TripsController, you might write tests to ensure that the index action returns a list of trips, and that the create action creates a new trip and redirects to the trip’s show page.

Here’s an example of how you might write specs for the TripsController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
require 'rails_helper'

RSpec.describe TripsController do
  let(:user) { create(:user) }
  let(:destination) { create(:destination) }
  let(:trip) { create(:trip, user: user, destinations: [destination]) }

  describe "GET #index" do
    it "returns a success response" do
      get :index
      expect(response).to be_successful
    end

    it "assigns all trips as @trips" do
      get :index
      expect(assigns(:trips)).to eq([trip])
    end
  end

  describe "GET #show" do
    it "returns a success response" do
      get :show, params: {id: trip.to_param}
      expect(response).to be_successful
    end

    it "assigns the requested trip as @trip" do
      get :show, params: {id: trip.to_param}
      expect(assigns(:trip)).to eq(trip)
    end
  end

  describe "GET #new" do
    it "returns a success response when user is logged in" do
      sign_in user
      get :new
      expect(response).to be_successful
    end

    it "redirects to sign in page when user is not logged in" do
      get :new
      expect(response).to redirect_to(new_user_session_path)
    end
  end

  describe "GET #edit" do
    it "returns a success response when user is logged in and is the trip's owner" do
      sign_in user
      get :edit, params: {id: trip.to_param}
      expect(response).to be_successful
    end

    it "redirects to trip's show page when user is not the trip's owner" do
      other_user = create(:user)
      sign_in other_user
      get :edit, params: {id: trip.to_param}
      expect(response).to redirect_to(trip_path(trip))
    end

    it "redirects to sign in page when user is not logged in" do
      get :edit, params: {id: trip.to_param}
      expect(response).to redirect_to(new_user_session_path)
    end
  end

  describe "POST #create" do
    context "with valid params" do
      it "creates a new Trip" do
        sign_in user
        expect {
          post :create, params: {trip: trip.attributes}
        }.to change(Trip, :count).by(1)
      end

      it "redirects to the created trip" do
        sign_in user
        post :create, params: {trip: trip.attributes}
        expect(response).to redirect_to(Trip.last)
      end
    end

    context "with invalid params" do
      it "does not create a new trip and returns unprocessable entity status code" do
        sign_in user
        expect { post :create, params: {trip: {}} }.to_not change(Trip, :count)
        expect(response).to have_http_status(:unprocessable_entity)
      end
    end
  end
end

In the example above, the first line creates a describe block for the TripsController. Within the describe block, you’ll write tests to check the behavior of the controller.

Next, we use the context method to specify different scenarios that we want to test.

For example, in the first context block, we test the behavior of the index action. Here, we create a before block to set up any data that is needed for the test. In this case, we create an instance variable @trips and assign it to an array of trips, which will be used in the controller’s index action.

After that, we use the it method to write a test that specifies what we expect to happen when the controller’s index action is executed. In this case, we expect the @trips instance variable to be assigned and for the index template to be rendered.

In the last context block, we test the behavior of the create action. This time, we use the post method to simulate a POST request to the create action and pass along the necessary parameters. We then use the expect method to test the response and check if the create action redirecteds to the trip’s show page. Note that we have a context for both with valid params and with invalid params cases and we have it blocks that specifies what to expect in each scenario.

While we didn’t cover every block of the TripsController spec, the examples provided give a good idea of how to write tests for your controllers in Rails. This knowledge should also make you able to write the specs for the DestinationsController and even for your on controllers in any of your Rails apps.

Conclusion

It’s important to test all aspects of your application to make sure it works as expected. By using RSpec to write efficient and comprehensive tests, you’ll be able to catch bugs and edge cases early on, which will save you time and headaches in the long run.

If you want to read more about tests in Ruby on Rails or any other aspect of software devlopment, career and tech news, be sure to subscribe to our blog newsletter to stay updated. And if you have any question or suggestion about this article, please leve a comment.

Happy testing!

Comments