Подтвердить что ты не робот

Как выполнить тест контроллера rspec put с эшафота

Я использую scaffolding для генерации тестов контроллера rspec. По умолчанию он создает тест как:

  let(:valid_attributes) {
    skip("Add a hash of attributes valid for your model")
  }

  describe "PUT update" do
    describe "with valid params" do
      let(:new_attributes) {
        skip("Add a hash of attributes valid for your model")
      }

      it "updates the requested doctor" do
        company = Company.create! valid_attributes
        put :update, {:id => company.to_param, :company => new_attributes}, valid_session
        company.reload
        skip("Add assertions for updated state")
      end

Используя FactoryGirl, я заполнил это с помощью:

  let(:valid_attributes) { FactoryGirl.build(:company).attributes.symbolize_keys }

  describe "PUT update" do
    describe "with valid params" do
      let(:new_attributes) { FactoryGirl.build(:company, name: 'New Name').attributes.symbolize_keys }

      it "updates the requested company", focus: true do
        company = Company.create! valid_attributes
        put :update, {:id => company.to_param, :company => new_attributes}, valid_session
        company.reload
        expect(assigns(:company).attributes.symbolize_keys[:name]).to eq(new_attributes[:name])

Это работает, но похоже, что я должен проверить все атрибуты, а не просто проверять измененное имя. Я попытался изменить последнюю строку на:

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

  expect(assigns(:company).attributes.delete_mutable_attributes.symbolize_keys).to eq(new_attributes)

Это почти сработало, но я получаю следующую ошибку от rspec, связанной с полями BigDecimal:

   -:latitude => #<BigDecimal:7fe376b430c8,'0.8137713195 830835E2',27(27)>,
   -:longitude => #<BigDecimal:7fe376b43078,'-0.1270954650 1027958E3',27(27)>,
   +:latitude => #<BigDecimal:7fe3767eadb8,'0.8137713195 830835E2',27(27)>,
   +:longitude => #<BigDecimal:7fe3767ead40,'-0.1270954650 1027958E3',27(27)>,

Использование rspec, factory_girl и scaffolding невероятно распространено, поэтому мои вопросы:

Каков хороший пример теста rspec и factory_girl для обновления PUT с допустимыми параметрами? Нужно ли использовать attributes.symbolize_keys и удалить изменяемые ключи? Как я могу получить эти объекты BigDecimal для оценки как eq?

4b9b3361

Ответ 1

Итак, так я и делаю, я не претендую на строгое соблюдение лучших практик, но я сосредоточен на точности моих тестов, ясности моего кода и быстром выполнении моего пакета.

Итак, возьмем пример UserController

1 - Я не использую FactoryGirl для определения атрибутов для отправки на мой контроллер, потому что я хочу сохранить эти атрибуты. FactoryGirl полезен для создания записи, но вы всегда должны вручную устанавливать данные, участвующие в тестируемой операции, лучше для удобства чтения и согласованности.

В этом отношении мы вручную определим опубликованные атрибуты

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

2 - Затем я определяю атрибуты, которые я ожидаю для обновленной записи, это может быть точная копия опубликованных атрибутов, но может быть, что контроллер выполняет некоторую дополнительную работу, и мы также хотим чтобы проверить это. Итак, скажем, для нашего примера, что, как только наш пользователь обновит свою личную информацию, наш контроллер автоматически добавит флаг need_admin_validation

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

То же, где вы можете добавить утверждение для атрибута, который должен оставаться неизменным. Пример с полем age, но это может быть что угодно

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

3 - Я определяю действие в блоке let. Вместе с предыдущими 2 let я считаю, что мои спецификации очень читаемы. А также легко написать shared_examples

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

4 - (с этого момента все в общем примере и пользовательские rspec-шаблоны в моих проектах) Время создания оригинальной записи, для чего мы можем использовать FactoryGirl

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

Как вы можете видеть, мы вручную установили значение для age, поскольку мы хотим проверить, что он не изменился во время действия update. Кроме того, даже если factory уже установил возраст до 25, я всегда перезаписываю его, поэтому мой тест не будет прерываться, если я изменю factory.

Вторая вещь, которую нужно отметить: здесь мы используем let! с треском. Это потому, что иногда вы можете протестировать свое действие отказа контроллера, и лучший способ сделать это - заглушить valid? и вернуть false. Как только вы закроете valid?, вы больше не сможете создавать записи для одного и того же класса, поэтому let! с ударом будет создавать запись перед заглушкой valid?

5 -. Сами утверждения (и, наконец, ответ на ваш вопрос)

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

Подведите итоги. Таким образом, добавив все вышеперечисленное, это выглядит так:

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 - это помощник, который упростит ваш rspec.

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

Как вы можете видеть с помощью этого простого помощника, когда мы ожидаем для BigDecimal, мы можем просто написать следующее, а помощник сделать остальные

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

Итак, в конце и в заключение, когда вы написали свои общие_примеры, помощники и пользовательские матчи, вы можете сохранить свои спецификации супер СУХОЙ. Как только вы начнете повторять то же самое в своих спецификациях контроллеров, найдите, как вы можете реорганизовать это. Это может занять некоторое время, но после его завершения вы можете написать тесты для всего контроллера за несколько минут.


И последнее слово (я не могу остановиться, я люблю Rspec), вот как выглядит мой полный помощник. Он полезен для чего угодно, а не только для моделей.

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

Ответ 2

Это сообщение для опроса. Мне пришлось немного спуститься по кроличьей дыре в понимании нескольких, перекрывающихся вопросов, поэтому я просто хотел сообщить о найденном решении.

TL;DR; Слишком много проблем, пытаясь подтвердить, что каждый важный атрибут возвращается без изменений из PUT. Просто проверьте, что измененный атрибут - это то, что вы ожидаете.

Проблемы, с которыми я столкнулся:

  • FactoryGirl.attributes_for не возвращает все значения, поэтому FactoryGirl: attributes_for не дает мне связанных атрибутов предлагает использовать (Factory.build :company).attributes.symbolize_keys, что приводит к возникновению новых проблем.
  • В частности, перечисления Rails 4.1 отображаются как целые числа вместо значений enum, как указано здесь: https://github.com/thoughtbot/factory_girl/issues/680
  • Оказалось, что проблема BigDecimal была красной селедкой, вызванной ошибкой в ​​совпадении rspec, который производит неправильные различия. Это было установлено здесь: https://github.com/rspec/rspec-core/issues/1649
  • Фактический сбой сочленения вызван значениями даты, которые не совпадают. Это связано с тем, что время возврата отличается от другого, но оно не отображается, потому что Date.inspect не показывает миллисекунды.
  • Я обошел эти проблемы с помощью метода Hash с обезьяной, который символизирует значения ключей и стробов.

Здесь метод Hash, который может идти в 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

В качестве альтернативы (и, возможно, предпочтительно) я мог бы написать собственный сопоставитель rspec, чем итерации через каждый атрибут, и сравнивал их значения по отдельности, что могло бы обойти проблему даты. Это был подход метода assert_records_values в нижней части ответа, выбранного мной @Benjamin_Sinclaire (для чего, спасибо).

Однако я решил вместо этого вернуться к гораздо более простому способу придерживаться attributes_for и просто сравнить атрибут, который я изменил. В частности:

  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

Я надеюсь, что этот пост позволит другим избежать повторения моих исследований.

Ответ 3

Ну, я сделал что-то более простое, я использую Fabricator, но я уверен, что это так же с 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

Кроме того, я не уверен, почему вы строите новый factory, PUT-глагол должен добавить новый материал, верно?. И то, что вы тестируете, если то, что вы добавили в первую очередь (new_attributes), существует после put в той же модели.

Ответ 4

Этот код может использоваться для решения ваших двух проблем:

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

Решает проблему сравнения чисел с плавающей запятой, преобразовывая значения в это строковое представление с помощью JSON.

Он также решает проблему проверки того, что новые значения были обновлены, но остальные атрибуты не изменились.

По моему опыту, по мере того как сложность возрастает, обычно нужно проверить конкретное состояние объекта, а не "ожидать, что атрибуты, которые я не обновляю, не изменятся". Представьте себе, например, что некоторые изменения атрибутов изменяются по мере того, как обновление выполняется в контроллере, например "оставшиеся элементы", "некоторые атрибуты состояния"... Вы хотели бы проверить конкретные ожидаемые изменения, которые могут быть больше, чем обновленные атрибуты.

Ответ 5

Вот мой способ тестирования PUT. Это фрагмент из моего notes_controller_spec, основная идея должна быть ясной (скажите, если нет):

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

Вместо FactoryGirl.build(:company).attributes.symbolize_keys я напишу FactoryGirl.attributes_for(:company). Он короче и содержит только параметры, указанные вами в factory.


К сожалению, это все, что я могу сказать о ваших вопросах.


P.S. Хотя, если вы положили проверку равенства BigDecimal на уровень базы данных, написав в стиле, например

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

это может сработать для вас.

Ответ 6

Тестирование приложения rails с помощью rspec-rails gem. Создал эшафот пользователя. Теперь вам нужно передать все примеры для user_controller_spec.rb

Это уже написано генератором эшафотов. Просто выполните

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

Теперь будет много примеров из этого файла.

Для invalid_attributes обязательно добавьте проверки в любое поле и

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

В модели пользователей. Валидация для first_name равна = >

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

Теперь все примеры, созданные генераторами, пройдут для этого controller_spec