Lucas Andrade
Lucas Andrade

Reputation: 4600

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: "[email protected]",
                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: "[email protected]",
                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: "[email protected]",
                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: "[email protected]",
                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: "[email protected]",
            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?

Upvotes: 1

Views: 1410

Answers (2)

Matheus Santana
Matheus Santana

Reputation: 581

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).

Upvotes: 2

jvillian
jvillian

Reputation: 20263

I think I would be tempted to do it something like below. Essentially:

  1. Create a memoized variable called @full_application and wrap it in a method (I've done this at the bottom of the test).

  2. Create constants stipulating the subsets of the values that you want for each test, such as STEP_ONE_PARAMS, STEP_TWO_PARAMS, etc.

  3. In each it block, use .slice and the constants defined above to "grab" the values from full_application that you want to use.

Something like this:

require 'rails_helper'

RSpec.describe Api::MenteeApplicationsController, type: :controller do

  STEP_ONE_PARAMS = %w(
    first_name
    last_name
    email
    gender
    country
    communicating_in_english
    send_to_mentor_confirmed
    time_availability
    previous_programming_experience
  ).freeze

  STEP_TWO_PARAMS = STEP_ONE_PARAMS.dup.concat(%w(
    motivation
    background
    team_work_experience
  )).freeze

  STEP_THREE_PARAMS = STEP_TWO_PARAMS.dup.concat(%w(
    operating_system
    project_proposal
    roadmap
    engagements
  )).freeze

    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: full_application.slice(*STEP_ONE_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: full_application.slice(*STEP_TWO_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: full_application.slice(*STEP_THREE_PARAMS),
            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: full_application,
            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


def full_application
  @full_application ||= {
    first_name:                       "Mentee", 
    last_name:                        "Rspec", 
    email:                            "[email protected]",
    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"
    ] 
  }
end

Upvotes: 0

Related Questions