Search code examples
ruby-on-railsrubytestingrspecrspec-rails

Rspec how to create an method to "DRY" only some params of a request?


I want to test a create method of my project, but this create method has 3 steps in my form and I want to test all of them. To test each step I need to send a create request with their respective params of the step.

The problem is: I am repeating many params in each step, I want to know how can I put the common params in a method and then just call it.

Here is my rspec file

require 'rails_helper'

RSpec.describe Api::MenteeApplicationsController, type: :controller do
    describe "Api Mentee Application controller tests" do
        let(:edition) { create(:edition) }

        it 'should start create a Mentee Application, step 1' do
            edition
            post :create, application: {
                first_name: "Mentee", last_name: "Rspec", email: "mentee@email.com",
                gender: "female", country: "IN", program_country: "IN",
                time_zone: "5 - Mumbai", communicating_in_english: "true",
                send_to_mentor_confirmed: "true",
                time_availability: 3,
                previous_programming_experience: "false" },
                step: "1", steps: "3"

            expect(response).to have_http_status(200)
        end

        it 'should continue to create a Mentee Application, step 2' do
            post :create, application: {
                first_name: "Mentee", last_name: "Rspec", email: "mentee@email.com",
                gender: "female", country: "IN", program_country: "IN",
                time_zone: "5 - Mumbai", communicating_in_english: "true",
                send_to_mentor_confirmed: "true",
                time_availability: 3,
                motivation: "Motivation",
                background: "Background",
                team_work_experience: "Team Work Experience",
                previous_programming_experience: "false" },
                step: "2", steps: "3"

            expect(response).to have_http_status(200)
        end

        it 'should not create a Mentee Application in api format' do
            applications = MenteeApplication.count
            post :create, application: {
                first_name: "Mentee", last_name: "Rspec", email: "mentee@email.com",
                gender: "female", country: "IN", program_country: "IN",
                time_zone: "5 - Mumbai", communicating_in_english: "true",
                send_to_mentor_confirmed: "true",
                motivation: "Motivation",
                background: "Background",
                team_work_experience: "Team Work Experience",
                previous_programming_experience: "false", experience: "",
                operating_system: "mac_os",
                project_proposal: "Project Proposal",
                roadmap: "Roadmap",
                time_availability: 3,
                engagements: ["master_student", "part_time", "volunteer", "one_project"] },
            step: "3", steps: "3"

            expect(response).to have_http_status(:unprocessable_entity)
            expect(MenteeApplication.count).to be(0)
        end

        it 'should create a Mentee Application in api format (step 3)' do
            applications = MenteeApplication.count
            post :create, application: {
                first_name: "Mentee", last_name: "Rspec", email: "mentee@email.com",
                gender: "female", country: "IN", program_country: "IN",
                time_zone: "5 - Mumbai", communicating_in_english: "true",
                send_to_mentor_confirmed: "true",
                motivation: "Motivation",
                background: "Background",
                programming_language: "ruby",
                team_work_experience: "Team Work Experience",
                previous_programming_experience: "false", experience: "",
                operating_system: "mac_os",
                project_proposal: "Project Proposal",
                roadmap: "Roadmap",
                time_availability: 3,
                engagements: ["master_student", "part_time", "volunteer", "one_project"] },
            step: "3", steps: "3"

            expect(response).to have_http_status(200)
            expect(MenteeApplication.count).to be(applications+1)
            expect(flash[:notice]).to eq("Thank you for your application!")
        end

    end
end

As you can see, the params in step 1 are used in steps 2 and 3, so I was thinking in something like this:

def some_params
    params.require(:application).permit(first_name: "Mentee", last_name: "Rspec", email: "mentee@email.com",
            gender: "female", country: "IN", program_country: "IN",
            time_zone: "5 - Mumbai", communicating_in_english: "true",
            send_to_mentor_confirmed: "true",
            time_availability: 3,
            previous_programming_experience: "false")
end

But didn't work, how can I do that?


Solution

  • let blocks allow you to define variables for using within the tests cases (its). Some key points to be aware of:

    • They are lazily evaluated: code within the block is not run until you call the variable (unless you use a bang -- let! -- which forces the evaluation)
    • They might be overridden within inner contexts

    Head to RSpec docs to know more about them.


    The code you provided could make use of lets just like this:

    require 'rails_helper'
    
    RSpec.describe Api::MenteeApplicationsController, type: :controller do
        describe "Api Mentee Application controller tests" do
            let(:edition) { create(:edition) }
            let(:first_step_params) do
              {
                first_name: 'Mentee',
                last_name: 'Rspec',
                #...
                previous_programming_experience: false,
              }
            end
            let(:second_step_params) do
              {
                motivation: "Motivation",
                background: "Background",
                team_work_experience: "Team Work Experience",
              }.merge(first_step_params)
            end
            let(:third_step_params) do
              {
                operating_system: "mac_os",
                project_proposal: "Project Proposal",
                roadmap: "Roadmap",
                time_availability: 3,
                engagements: ["master_student", "part_time", "volunteer", "one_project"],
              }.merge(third_step_params)
            end
    
            it 'should start create a Mentee Application, step 1' do
                edition                                                          
    
                post :create, application: first_step_params, step: "1", steps: "3"
    
                expect(response).to have_http_status(200)                        
            end                                                                  
    
            it 'should continue to create a Mentee Application, step 2' do       
                post :create, application: second_step_params, step: "2", steps: "3"
    
                expect(response).to have_http_status(200)                        
            end
    
            it 'should not create a Mentee Application in api format' do
                applications = MenteeApplication.count
    
                post :create, application: third_step_params, step: "3", steps: "3"
    
                expect(response).to have_http_status(:unprocessable_entity)
                expect(MenteeApplication.count).to be(0)
            end
        end
    end
    

    Additional suggestions

    1. Do not implement controller specs

    Controllers are meant to be a thin software layer between the user interface and background services. Their tests can hardly be acknowledged as integration (end-to-end) nor unit tests.

    I'd suggest you to implement feature specs instead. (capybara is a great match for Rails testing with RSpec)

    This blog post might provide more insights on this.

    2. Do not use should in your test cases descriptions

    See betterspecs.org.

    3. Mind the last trailing comma in

    let(:application_params) do                                                      
      {                                                                  
        first_name: 'Mentee',                                            
        last_name: 'Rspec',                                              
        #...                          
        previous_programming_experience: false,
      }                                                                  
    end
    

    It prevents incidental changes.

    4. Use a .rspec file

    With contents such as

    --require rails_helper
    

    So you don't need require 'rails_helper' on top of each spec file.

    5. Use contexts

    This is also a guidance from betterspecs.org. You could do something like

    RSpec.describe Api::MenteeApplicationsController, type: :controller do
        describe "Api Mentee Application controller tests" do
            let(:edition) { create(:edition) }
            let(:application_params) do
              {
                #...
              }
            end
            let(:step) { 1 }
    
            it 'should start create a Mentee Application' do
                edition
    
                post :create, application: application_params, step: step, steps: "3"
    
                expect(response).to have_http_status(200)
            end
    
            context 'in second step' do
              let(:step) { 2 }
    
              it 'should continue to create a Mentee Application' do
                  post :create, application: application_params, step: step, steps: "3"
    
                  expect(response).to have_http_status(200)
              end
            end
        end
    end
    

    contexts might also be handy for handling additional params:

    RSpec.describe Api::MenteeApplicationsController, type: :controller do
      describe "Api Mentee Application controller tests" do
        let(:edition) { create(:edition) }
        let(:application_params) do
          common_params.merge(additional_params)
        end
        let(:commom_params) do
          {
            #...
          }
        end
        let(:additional_params) { {} }
    
        it 'creates an application' do
          post :create, application: application_params
        end
    
        context 'with API params' do
          let(:additional_params) do
            {
              #...
            }
          end
    
          it 'creates an application' do
            post :create, application: application_params
          end
        end
      end
    end
    

    Note that the post method call became exactly the same in both contexts. This would allow for reusing it (in a before block or even another let block).