How to complete the rspec put controller test from scaffold

前端 未结 6 1319
Happy的楠姐
Happy的楠姐 2020-12-24 11:58

I\'m using scaffolding to generate rspec controller tests. By default, it creates the test as:

  let(:valid_attributes) {
    skip(\"Add a hash of attributes         


        
相关标签:
6条回答
  • 2020-12-24 12:39

    Here is my way of testing PUT. That is a snippet from my notes_controller_spec, the main idea should be clear (tell me if not):

    RSpec.describe NotesController, :type => :controller do
      let(:note) { FactoryGirl.create(:note) }
      let(:valid_note_params) { FactoryGirl.attributes_for(:note) }
      let(:request_params) { {} }
    
      ...
    
      describe "PUT 'update'" do
        subject { put 'update', request_params }
    
        before(:each) { request_params[:id] = note.id }
    
        context 'with valid note params' do
          before(:each) { request_params[:note] = valid_note_params }
    
          it 'updates the note in database' do
            expect{ subject }.to change{ Note.where(valid_note_params).count }.by(1)
          end
        end
      end
    end
    

    Instead of FactoryGirl.build(:company).attributes.symbolize_keys, I'd write FactoryGirl.attributes_for(:company). It is shorter and contains only parameters that you specified in your factory.


    Unfortunately that is all I can say about your questions.


    P.S. Though if you lay BigDecimal equality check on database layer by writing in style like

    expect{ subject }.to change{ Note.where(valid_note_params).count }.by(1)
    

    this may work for you.

    0 讨论(0)
  • 2020-12-24 12:42

    This is the questioner posting. I had to go down the rabbit hole a bit in understanding multiple, overlapping issues here, so I just wanted to report back on the solution I found.

    tldr; It's too much trouble trying to confirm that every important attribute comes back unchanged from a PUT. Just check that the changed attribute is what you expect.

    The issues I encountered:

    1. FactoryGirl.attributes_for does not return all values, so FactoryGirl: attributes_for not giving me associated attributes suggests using (Factory.build :company).attributes.symbolize_keys, which winds up creating new problems.
    2. Specifically, Rails 4.1 enums show as integers instead of enum values, as reported here: https://github.com/thoughtbot/factory_girl/issues/680
    3. It turns out that the BigDecimal issue was a red herring, caused by a bug in the rspec matcher which produces incorrect diffs. This was established here: https://github.com/rspec/rspec-core/issues/1649
    4. The actual matcher failure is caused by Date values that don't match. This is due to the time returned being different, but it doesn't show because Date.inspect does not show milliseconds.
    5. I got around these problems with a monkey patched Hash method that symbolizes keys and stringifes values.

    Here's the Hash method, which could go in rails_spec.rb:

    class Hash
      def symbolize_and_stringify
        Hash[
          self
          .delete_if { |k, v| %w[id created_at updated_at].member?(k) }
          .map { |k, v| [k.to_sym, v.to_s] }
        ]
      end
    end
    

    Alternatively (and perhaps preferably) I could have written a custom rspec matcher than iterates through each attribute and compares their values individually, which would have worked around the date issue. That was the approach of the assert_records_values method at the bottom of the answer I selected by @Benjamin_Sinclaire (for which, thank you).

    However, I decided instead to go back to the much, much simpler approach of sticking with attributes_for and just comparing the attribute I changed. Specifically:

      let(:valid_attributes) { FactoryGirl.attributes_for(:company) }
      let(:valid_session) { {} }
    
      describe "PUT update" do
        describe "with valid params" do
          let(:new_attributes) { FactoryGirl.attributes_for(:company, name: 'New Name') }
    
          it "updates the requested company" do
            company = Company.create! valid_attributes
            put :update, {:id => company.to_param, :company => new_attributes}, valid_session
            company.reload
            expect(assigns(:company).attributes['name']).to match(new_attributes[:name])
          end
    

    I hope this post allows others to avoid repeating my investigations.

    0 讨论(0)
  • 2020-12-24 12:45

    Testing the rails application with rspec-rails gem. Created the scaffold of user. Now you need to pass all the examples for the user_controller_spec.rb

    This has already written by the scaffold generator. Just implement

    let(:valid_attributes){ hash_of_your_attributes} .. like below
    let(:valid_attributes) {{ first_name: "Virender", last_name: "Sehwag", gender: "Male"}
      } 
    

    Now will pass many examples from this file.

    For invalid_attributes be sure to add the validations on any of field and

    let(:invalid_attributes) {{first_name: "br"}
      }
    

    In the users model .. validation for first_name is as =>

      validates :first_name, length: {minimum: 5}, allow_blank: true
    

    Now all the examples created by the generators will pass for this controller_spec

    0 讨论(0)
  • 2020-12-24 12:54

    This code can be used to solve your two issues:

    it "updates the requested patient" do
      patient = Patient.create! valid_attributes
      patient_before = JSON.parse(patient.to_json).symbolize_keys
      put :update, { :id => patient.to_param, :patient => new_attributes }, valid_session
      patient.reload
      patient_after = JSON.parse(patient.to_json).symbolize_keys
      patient_after.delete(:updated_at)
      patient_after.keys.each do |attribute_name|
        if new_attributes.keys.include? attribute_name
          # expect updated attributes to have changed:
          expect(patient_after[attribute_name]).to eq new_attributes[attribute_name].to_s
        else
          # expect non-updated attributes to not have changed:
          expect(patient_after[attribute_name]).to eq patient_before[attribute_name]
        end
      end
    end
    

    It solves the problem of comparing floating point numbers by converting the values to it string representation using JSON.

    It also solves the problem of checking that the new values have been updated but the rest of the attributes have not changed.

    In my experience, though, as the complexity grows, the usual thing to do is to check some specific object state instead of "expecting that the attributes I don't update won't change". Imagine, for instance, having some other attributes changing as the update is done in the controller, like "remaining items", "some status attributes"... You would like to check the specific expected changes, that may be more than the updated attributes.

    0 讨论(0)
  • 2020-12-24 12:56

    Well, I did something that's quite simpler, I'm using Fabricator, but I'm pretty sure it's the same with FactoryGirl:

      let(:new_attributes) ( { "phone" => 87276251 } )
    
      it "updates the requested patient" do
        patient = Fabricate :patient
        put :update, id: patient.to_param, patient: new_attributes
        patient.reload
        # skip("Add assertions for updated state")
        expect(patient.attributes).to include( { "phone" => 87276251 } )
      end
    

    Also, I'm not sure why you are building a new factory, PUT verb is supposed to add new stuff, right?. And what you are testing if what you added in the first place (new_attributes), happens to exist after the put in the same model.

    0 讨论(0)
  • 2020-12-24 13:01

    Ok so this is how I do, I don't pretend to strictly follow the best practices, but I focus on precision of my tests, clarity of my code, and fast execution of my suite.

    So let take example of a UserController

    1- I do not use FactoryGirl to define the attributes to post to my controller, because I want to keep control of those attributes. FactoryGirl is useful to create record, but you always should set manually the data involved in the operation you are testing, it's better for readability and consistency.

    In this regard we will manually define the posted attributes

    let(:valid_update_attributes) { {first_name: 'updated_first_name', last_name: 'updated_last_name'} }
    

    2- Then I define the attributes I expect for the updated record, it can be an exact copy of the posted attributes, but it can be that the controller do some extra work and we also want to test that. So let's say for our example that once our user updated his personal information our controller automatically add a need_admin_validation flag

    let(:expected_update_attributes) { valid_update_attributes.merge(need_admin_validation: true) }
    

    That's also where you can add assertion for attribute that must remain unchanged. Example with the field age, but it can be anything

    let(:expected_update_attributes) { valid_update_attributes.merge(age: 25, need_admin_validation: true) }
    

    3- I define the action, in a let block. Together with the previous 2 let I find it makes my specs very readable. And it also make easy to write shared_examples

    let(:action) { patch :update, format: :js, id: record.id, user: valid_update_attributes }
    

    4- (from that point everything is in shared example and custom rspec matchers in my projects) Time to create the original record, for that we can use FactoryGirl

    let!(:record) { FactoryGirl.create :user, :with_our_custom_traits, age: 25 }
    

    As you can see we manually set the value for age as we want to verify it did not change during the update action. Also, even if the factory already set the age to 25 I always overwrite it so my test won't break if I change the factory.

    Second thing to note: here we use let! with a bang. That is because sometimes you may want to test your controller's fail action, and the best way to do that is to stub valid? and return false. Once you stub valid? you can't create records for the same class anymore, therefor let! with a bang would create the record before the stub of valid?

    5- The assertions itself (and finally the answer to your question)

    before { action }
    it {
      assert_record_values record.reload, expected_update_attributes
      is_expected.to redirect_to(record)
      expect(controller.notice).to eq('User was successfully updated.')
    }
    

    Summarize So adding all the above, this is how the spec looks like

    describe 'PATCH update' do
      let(:valid_update_attributes) { {first_name: 'updated_first_name', last_name: 'updated_last_name'} }
      let(:expected_update_attributes) { valid_update_attributes.merge(age: 25, need_admin_validation: true) }
      let(:action) { patch :update, format: :js, id: record.id, user: valid_update_attributes }
      let(:record) { FactoryGirl.create :user, :with_our_custom_traits, age: 25 }
      before { action }
      it {
        assert_record_values record.reload, expected_update_attributes
        is_expected.to redirect_to(record)
        expect(controller.notice).to eq('User was successfully updated.')
      }
    end
    

    assert_record_values is the helper that will make your rspec simpler.

    def assert_record_values(record, values)
      values.each do |field, value|
        record_value = record.send field
        record_value = record_value.to_s if (record_value.is_a? BigDecimal and value.is_a? String) or (record_value.is_a? Date and value.is_a? String)
    
        expect(record_value).to eq(value)
      end
    end
    

    As you can see with this simple helper when we expect for a BigDecimal, we can just write the following, and the helper do the rest

    let(:expected_update_attributes) { {latitude: '0.8137713195'} }
    

    So at the end, and to conclude, when you have written your shared_examples, helpers, and custom matchers, you can keep your specs super DRY. As soon as you start repeating the same thing in your controllers specs find how you can refactor this. It may take time at first, but when its done you can write the tests for a whole controller in few minutes


    And a last word (I can't stop, I love Rspec) here is how my full helper look like. It is usable for anything in fact, not just models.

    def assert_records_values(records, values)
      expect(records.length).to eq(values.count), "Expected <#{values.count}> number of records, got <#{records.count}>\n\nRecords:\n#{records.to_a}"
      records.each_with_index do |record, index|
        assert_record_values record, values[index], index: index
      end
    end
    
    def assert_record_values(record, values, index: nil)
      values.each do |field, value|
        record_value = [field].flatten.inject(record) { |object, method| object.try :send, method }
        record_value = record_value.to_s if (record_value.is_a? BigDecimal and value.is_a? String) or (record_value.is_a? Date and value.is_a? String)
    
        expect_string_or_regexp record_value, value,
                                "#{"(index #{index}) " if index}<#{field}> value expected to be <#{value.inspect}>. Got <#{record_value.inspect}>"
      end
    end
    
    def expect_string_or_regexp(value, expected, message = nil)
      if expected.is_a? String
        expect(value).to eq(expected), message
      else
        expect(value).to match(expected), message
      end
    end
    
    0 讨论(0)
提交回复
热议问题