1

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?

Lucas Andrade
  • 4,315
  • 5
  • 29
  • 50

2 Answers2

2

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

Matheus Santana
  • 581
  • 1
  • 6
  • 22
  • This however doesn't provide the new application params. – 3limin4t0r Mar 14 '18 at 13:22
  • Thanks @JohanWentholt. I've updated the answer. What do you think? – Matheus Santana Mar 14 '18 at 13:30
  • I don't want to be the annoying one, but you've added the step 3 params, but still left out step 2. – 3limin4t0r Mar 14 '18 at 13:48
  • @MatheusSantana this works for step 1 but I can't make it work for step 2 and 3. Code of step 2: `post :create, application: application_params { motivation: "Motivation", background: "Background", team_work_experience: "Team Work Experience" }, step: "2", steps: "3"` – Lucas Andrade Mar 14 '18 at 13:58
  • @LucasAndrade @JohanWentholt I'm sorry, I hadn't noticed the differing params for these steps. I've updated the answer (but the main idea remains the same: using `let`s). Does it work now? – Matheus Santana Mar 14 '18 at 14:05
  • @MatheusSantana I deleted my answer since it doesn't add anything anymore. Your answer seems pretty solid now. (Y) – 3limin4t0r Mar 14 '18 at 15:00
  • 1
    One last thing to keep in mind by having the merge with the previous params at the end, is that it overwrites the hash you're creating. This means if you have two conflicting keys the value of the previous params is chosen. If you don't want this behavior you have to change the order and call _#merge_ on the previous params, supplying the new additions. – 3limin4t0r Mar 14 '18 at 15:04
0

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:                            "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"
    ] 
  }
end
jvillian
  • 19,953
  • 5
  • 31
  • 44
  • Why use `arr1.dup.concat(arr2)` over `arr1 + arr2`? Or did you not know about the `+` method on arrays? – 3limin4t0r Mar 14 '18 at 13:50
  • Oh, wow! There's a `+` method on arrays?!? BTW, `concat` and `+` are not the same. [Here's](https://stackoverflow.com/questions/1801516/how-do-you-add-an-array-to-another-array-in-ruby-and-not-end-up-with-a-multi-dim) one Q&A on the topic, although there are a lot of them out there. In this particular instance, the OP could probably go either way without great consequence. – jvillian Mar 14 '18 at 14:01