webentwicklung-frage-antwort-db.com.de

So beenden Sie den rspec-Controller-Test vom Gerüst aus

Ich verwende ein Gerüst, um Rspec-Controller-Tests zu generieren. Standardmäßig wird der Test erstellt als:

  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

Mit FactoryGirl habe ich dies mit:

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

Dies funktioniert, aber es scheint, als ob ich in der Lage sein sollte, alle Attribute zu testen, anstatt nur den geänderten Namen zu testen. Ich habe versucht, die letzte Zeile zu ändern:

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)

Das hat fast funktioniert, aber ich bekomme den folgenden Fehler von rspec, der mit BigDecimal-Feldern zu tun hat:

   -: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)>,

Die Verwendung von rspec, factory_girl und Gerüsten ist unglaublich häufig, daher sind meine Fragen:

Was ist ein gutes Beispiel für einen rspec- und factory_girl-Test für ein PUT-Update mit gültigen Parametern? Ist es notwendig, attributes.symbolize_keys zu verwenden und die veränderbaren Schlüssel zu löschen? Wie kann ich diese BigDecimal-Objekte als eq auswerten lassen?

26
dankohn

Ok, so mache ich das, ich gebe nicht vor, die Best Practices strikt zu befolgen, aber ich konzentriere mich auf die Präzision meiner Tests, die Klarheit meines Codes und die schnelle Ausführung meiner Suite.

Nehmen wir ein Beispiel einer UserController

1- Ich benutze FactoryGirl nicht zum Definieren der Attribute, die an meinen Controller gesendet werden sollen, da ich die Kontrolle über diese Attribute behalten möchte. FactoryGirl ist nützlich, um Datensätze zu erstellen. Sie sollten jedoch immer die Daten manuell festlegen, die an dem von Ihnen getesteten Vorgang beteiligt sind. Dies ist aus Gründen der Lesbarkeit und Konsistenz besser.

In dieser Hinsicht definieren wir die veröffentlichten Attribute manuell

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

2- Dann definiere ich die Attribute, die ich für den aktualisierten Datensatz erwarte, es kann eine exakte Kopie der veröffentlichten Attribute sein, aber es kann sein, dass der Controller zusätzliche Arbeit leistet, und wir möchten das auch testen. Nehmen wir für unser Beispiel an, dass unser Controller nach der Aktualisierung seiner persönlichen Daten automatisch ein need_admin_validation-Flag hinzufügt

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

Dort können Sie auch Assertion für Attribute hinzufügen, die unverändert bleiben müssen. Beispiel mit dem Feld age, es kann jedoch alles sein

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

3- Ich definiere die Aktion in einem let-Block. Zusammen mit der vorherigen 2 let finde ich meine Angaben sehr lesbar. Und es macht es auch einfach, shared_examples zu schreiben

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

4- (ab diesem Punkt ist alles in gemeinsam genutzten Beispiel- und benutzerdefinierten rspec-Matchern in meinen Projekten enthalten) Zeit, um den ursprünglichen Datensatz zu erstellen, dafür können wir FactoryGirl verwenden

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

Wie Sie sehen, setzen wir den Wert für age manuell, da wir sicherstellen möchten, dass er sich während der update-Aktion nicht geändert hat. Auch wenn die Fabrik das Alter bereits auf 25 festgelegt hat, überschreibe ich sie immer, damit der Test nicht abgebrochen wird, wenn ich die Fabrik wechsle.

Zweitens zu beachten: Hier verwenden wir let! mit einem Knall. Das liegt daran, dass Sie manchmal die Fail-Aktion Ihres Controllers testen möchten. Die beste Möglichkeit ist, valid? zu stubben und false zurückzugeben. Sobald Sie valid? stubben, können Sie keine Datensätze für dieselbe Klasse mehr erstellen. Daher würde let! mit einem Knall den Datensatz before der Stub von valid? erstellen.

5- Die Behauptungen selbst (und schließlich die Antwort auf Ihre Frage) 

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

Zusammenfassen So fügen Sie alle oben genannten Punkte hinzu, so sieht die Spezifikation aus

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 ist der Helfer, der Ihre Rspec vereinfacht.

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

Wie Sie mit diesem einfachen Helfer sehen können, wenn wir eine BigDecimal erwarten, können wir einfach folgendes schreiben, und der Helfer erledigt den Rest

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

Am Ende, und zum Schluss, wenn Sie Ihre shared_examples, Helfer und benutzerdefinierten Matcher geschrieben haben, können Sie Ihre Daten super trocken halten. Sobald Sie anfangen, das Gleiche in Ihren Controllern zu wiederholen, erfahren Sie, wie Sie dies umgestalten können. Es kann etwas Zeit in Anspruch nehmen, aber wenn Sie fertig sind, können Sie die Tests für einen ganzen Controller in wenigen Minuten schreiben


Und ein letztes Wort (ich kann nicht aufhören, ich liebe Rspec). So sieht mein voller Helfer aus. Es kann für alles verwendet werden, nicht nur für Modelle.

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
30
Benj

Dies ist der Fragesteller. Ich musste ein wenig durch das Kaninchenloch gehen, um mehrere überlappende Probleme zu verstehen, also wollte ich nur über die Lösung berichten, die ich gefunden habe.

tldr; Es ist zu viel Mühe zu versuchen zu bestätigen, dass jedes wichtige Attribut unverändert von einem PUT zurückkommt. Überprüfen Sie einfach, ob das geänderte Attribut Ihren Erwartungen entspricht.

Die Probleme, auf die ich gestoßen bin:

  1. FactoryGirl.attributes_for gibt nicht alle Werte zurück, also FactoryGirl: attributes_for gibt mir keine zugehörigen Attribute schlägt die Verwendung von (Factory.build :company).attributes.symbolize_keys vor, wodurch neue Probleme entstehen.
  2. Insbesondere werden die Enumerationen von Rails 4.1 als Ganzzahlen anstelle von Enummenwerten angezeigt, wie hier berichtet: https://github.com/thoughtbot/factory_girl/issues/680
  3. Es stellte sich heraus, dass das BigDecimal-Problem ein roter Hering war, der durch einen Fehler im rspec-Matcher verursacht wurde, der falsche Diffs hervorbrachte. Dies wurde hier festgelegt: https://github.com/rspec/rspec-core/issues/1649
  4. Der tatsächliche Matcher-Fehler wird durch nicht übereinstimmende Datumswerte verursacht. Dies liegt daran, dass die zurückgegebene Zeit unterschiedlich ist, aber es wird nicht angezeigt, da Date.inspect keine Millisekunden anzeigt.
  5. Ich bin diesen Problemen mit einer mit Affen gepatchten Hash-Methode begegnet, die die Werte von key und stringifes symbolisiert.

Hier ist die Hash-Methode, die in Rails_spec.rb gehen könnte:

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

Alternativ (und möglicherweise vorzugsweise) hätte ich einen benutzerdefinierten rspec-Matcher schreiben können, der jedes Attribut durchläuft und seine Werte einzeln vergleicht, was die Datumsausgabe umgangen hätte. Das war der Ansatz der assert_records_values-Methode am Ende der Antwort, die ich von @Benjamin_Sinclaire ausgewählt habe (wofür danke).

Ich entschied mich jedoch, stattdessen zu dem viel einfacheren Ansatz zurückzukehren, bei attributes_for zu bleiben und nur das von mir geänderte Attribut zu vergleichen. Speziell:

  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

Ich hoffe, dass dieser Beitrag es anderen ermöglicht, meine Untersuchungen nicht zu wiederholen.

5
dankohn

Nun, ich habe etwas ganz einfaches gemacht, ich verwende Fabricator, bin aber ziemlich sicher, dass es bei FactoryGirl genauso ist:

  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

Ich bin mir auch nicht sicher, warum Sie eine neue Fabrik bauen. PUT-Verb soll neues Zeug hinzufügen, oder? Und was Sie testen, wenn das, was Sie an erster Stelle hinzugefügt haben (new_attributes), nach der put in demselben Modell existiert.

Dieser Code kann verwendet werden, um Ihre zwei Probleme zu lösen:

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

Das Problem des Vergleichs von Gleitkommazahlen wird gelöst, indem die Werte mithilfe von JSON in die entsprechende Zeichenfolgendarstellung konvertiert werden.

Es löst auch das Problem, zu prüfen, ob die neuen Werte aktualisiert wurden, die übrigen Attribute wurden jedoch nicht geändert.

Nach meiner Erfahrung ist es jedoch mit zunehmender Komplexität üblich, einen bestimmten Objektstatus zu prüfen, anstatt "zu erwarten, dass sich die Attribute, die ich nicht aktualisiere, nicht ändern". Stellen Sie sich zum Beispiel vor, dass sich andere Attribute ändern, während die Aktualisierung im Controller durchgeführt wird, wie "verbleibende Elemente", "einige Statusattribute" ... Sie möchten die spezifischen erwarteten Änderungen überprüfen, die möglicherweise mehr sind als die aktualisierten Attribute.

2
chipairon

Hier ist meine Art, PUT zu testen. Das ist ein Ausschnitt aus meinem notes_controller_spec, die Hauptidee sollte klar sein (sagen Sie mir, wenn nicht):

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

Anstelle von FactoryGirl.build(:company).attributes.symbolize_keys würde ich FactoryGirl.attributes_for(:company) schreiben. Es ist kürzer und enthält nur Parameter, die Sie in Ihrer Fabrik angegeben haben.


Das ist leider alles, was ich über Ihre Fragen sagen kann.


P.S. Wenn Sie jedoch die BigDecimal-Gleichheitsprüfung auf der Datenbankebene ablegen, schreiben Sie im Stil wie

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

das kann für Sie arbeiten.

1
nsave

Testen der Rails-Anwendung mit rspec-Rails gem. Das Gerüst des Benutzers wurde erstellt. Nun müssen Sie alle Beispiele für user_controller_spec.rb übergeben

Dies hat der Gerüstgenerator bereits geschrieben. Einfach umsetzen

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

Nun werden viele Beispiele aus dieser Datei übergeben.

Für invalid_attributes müssen Sie unbedingt die Validierungen in einem der Felder und einfügen 

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

Im Benutzermodell lautet die Validierung für first_name wie => 

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

Nun werden alle von den Generatoren erstellten Beispiele für diese controller_spec übergeben

0