ruby on rails - rails - Cómo completar la prueba del controlador rspec put desde un andamio
rspec rails 5 (6)
Aquí está mi manera de probar PUT. Ese es un fragmento de mi notes_controller_spec
, la idea principal debe ser clara (dime si no):
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
En lugar de FactoryGirl.build(:company).attributes.symbolize_keys
, escribiría FactoryGirl.attributes_for(:company)
. Es más corto y contiene solo los parámetros que especificó en su fábrica.
Desafortunadamente, eso es todo lo que puedo decir acerca de sus preguntas.
PD: aunque si coloca BigDecimal, compruebe la igualdad en la capa de base de datos escribiendo con estilo
expect{ subject }.to change{ Note.where(valid_note_params).count }.by(1)
Esto puede funcionar para usted.
Estoy usando andamios para generar pruebas de controlador rspec. Por defecto, crea la prueba como:
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
Usando FactoryGirl, he completado esto con:
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])
Esto funciona, pero parece que debería poder probar todos los atributos, en lugar de solo probar el nombre cambiado. Intenté cambiar la última línea a:
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)
Casi funciona, pero recibo el siguiente error de rspec relacionado con los campos 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)>,
Usar rspec, factory_girl y andamios es increíblemente común, por lo que mis preguntas son:
¿Cuál es un buen ejemplo de una prueba rspec y factory_girl para una actualización PUT con parámetros válidos? ¿Es necesario usar attributes.symbolize_keys
y eliminar las claves mutables? ¿Cómo puedo obtener esos objetos BigDecimal para evaluar como eq
?
Bueno, hice algo que es bastante más simple, estoy usando Fabricator, pero estoy bastante seguro de que es lo mismo con 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
Además, no estoy seguro de por qué estás construyendo una nueva fábrica, se supone que el verbo PUT agrega nuevas cosas, ¿no? Y lo que estás probando si lo que agregaste en primer lugar ( new_attributes
), existe después de la new_attributes
en el mismo modelo.
Esta es la publicación del interrogador. Tuve que ir un poco por el agujero del conejo para comprender múltiples problemas superpuestos aquí, así que solo quería informar sobre la solución que encontré.
tldr; Es demasiado complicado tratar de confirmar que todos los atributos importantes vuelven sin cambios desde un PUT. Solo verifica que el atributo cambiado sea lo que esperas.
Los problemas que encontré:
- FactoryGirl.attributes_for no devuelve todos los valores, por lo que FactoryGirl: attributes_for no me da los atributos asociados sugiere usar
(Factory.build :company).attributes.symbolize_keys
, que termina creando nuevos problemas. - Específicamente, las enumeraciones de Rails 4.1 se muestran como enteros en lugar de valores de enumeración, como se informa aquí: https://github.com/thoughtbot/factory_girl/issues/680
- Resulta que el problema de BigDecimal era una pista falsa, causada por un error en el emparejador rspec que produce diferencias incorrectas. Esto se estableció aquí: https://github.com/rspec/rspec-core/issues/1649
- La falla del emparejador real es causada por los valores de fecha que no coinciden. Esto se debe a que el tiempo devuelto es diferente, pero no se muestra porque
Date.inspect
no muestra milisegundos. - Resolví estos problemas con un método Hash parcheado que simboliza las claves y los valores de cadena de caracteres.
Aquí está el método Hash, que podría ir en 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
Alternativamente (y tal vez de preferencia) podría haber escrito un emparejador de rspec personalizado que itera a través de cada atributo y compara sus valores individualmente, lo que habría funcionado en torno a la fecha de emisión. Ese fue el enfoque del método assert_records_values
en la parte inferior de la respuesta que seleccioné por @Benjamin_Sinclaire (por lo cual, gracias).
Sin embargo, decidí, en cambio, volver al enfoque mucho más simple de seguir con los attributes_for
y simplemente comparar el atributo que cambié. Específicamente:
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
Espero que este post permita a otros evitar repetir mis investigaciones.
Este código se puede utilizar para resolver sus dos problemas:
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
Resuelve el problema de comparar números de punto flotante convirtiendo los valores en una representación de cadena utilizando JSON.
También resuelve el problema de verificar que los nuevos valores se hayan actualizado pero que el resto de los atributos no hayan cambiado.
Sin embargo, según mi experiencia, a medida que aumenta la complejidad, lo habitual es verificar el estado de un objeto específico en lugar de "esperar que los atributos que no actualizo no cambien". Imagine, por ejemplo, que algunos otros atributos cambien a medida que se realiza la actualización en el controlador, como "elementos restantes", "algunos atributos de estado" ... Le gustaría verificar los cambios específicos esperados, que pueden ser más que los actualizados atributos
Ok, así es como lo hago, no pretendo seguir estrictamente las mejores prácticas, pero me concentro en la precisión de mis pruebas, la claridad de mi código y la rápida ejecución de mi suite.
Así que vamos a tomar el ejemplo de un UserController
1- No uso FactoryGirl para definir los atributos para publicar en mi controlador, porque quiero mantener el control de esos atributos. FactoryGirl es útil para crear registros, pero siempre debe configurar manualmente los datos involucrados en la operación que está probando, es mejor para la legibilidad y la coherencia.
En este sentido definiremos manualmente los atributos publicados.
let(:valid_update_attributes) { {first_name: ''updated_first_name'', last_name: ''updated_last_name''} }
2- Luego defino los atributos que espero para el registro actualizado, puede ser una copia exacta de los atributos publicados, pero puede ser que el controlador haga un trabajo adicional y también queremos probar eso. Digamos, por ejemplo, que una vez que nuestro usuario actualizó su información personal, nuestro controlador agregará automáticamente un need_admin_validation
let(:expected_update_attributes) { valid_update_attributes.merge(need_admin_validation: true) }
También es donde puede agregar una aserción para un atributo que debe permanecer sin cambios. Ejemplo con la age
campo, pero puede ser cualquier cosa.
let(:expected_update_attributes) { valid_update_attributes.merge(age: 25, need_admin_validation: true) }
3- Defino la acción, en un bloque let
. Junto con los 2 anteriores, let
encontrar que hace que mis especificaciones sean muy legibles. Y también hace fácil escribir ejemplos_compliados
let(:action) { patch :update, format: :js, id: record.id, user: valid_update_attributes }
4- (a partir de ese momento, todo está en ejemplos compartidos y concordantes de rspec personalizados en mis proyectos) Es hora de crear el registro original, para eso podemos usar FactoryGirl
let!(:record) { FactoryGirl.create :user, :with_our_custom_traits, age: 25 }
Como puede ver, configuramos manualmente el valor para la age
ya que queremos verificar que no haya cambiado durante la acción de update
. Además, incluso si la fábrica ya estableció la edad en 25, siempre la sobrescribo para que mi prueba no se rompa si cambio la fábrica.
Lo segundo a tener en cuenta: aquí usamos let!
con una explosión. Esto se debe a que a veces es posible que desee probar la acción fallida de su controlador, y la mejor manera de hacerlo es aplazar la valid?
y devuelve falso. ¿Una vez que aparezcas valid?
¡Ya no puedes crear registros para la misma clase, por lo tanto, let!
con una explosión crearía el registro antes de la colilla de valid?
5- Las afirmaciones en sí (y finalmente la respuesta a tu pregunta)
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.'')
}
Resumir Entonces, al agregar todo lo anterior, así es como se ve la especificación
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
es el ayudante que simplificará su 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
Como puede ver con este simple ayudante cuando esperamos un BigDecimal
, podemos escribir lo siguiente y el ayudante hacer el resto
let(:expected_update_attributes) { {latitude: ''0.8137713195''} }
Así que al final, y para concluir, cuando haya escrito sus ejemplos compartidos, ayudantes y emparejadores personalizados, puede mantener sus especificaciones super DRY. Tan pronto como empieces a repetir lo mismo en las especificaciones de tus controladores, encuentra cómo puedes refactorizar esto. Puede llevar tiempo al principio, pero cuando haya terminado, puede escribir las pruebas para un controlador completo en pocos minutos
Y una última palabra (no puedo parar, me encanta Rspec) aquí es cómo se ve mi ayudante completo. Es utilizable para cualquier cosa, no solo modelos.
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
Probando la aplicación de rieles con rspec-rails gema. Creado el andamio del usuario. Ahora necesitas pasar todos los ejemplos para el user_controller_spec.rb
Esto ya lo ha escrito el generador de andamios. Solo implementa
let(:valid_attributes){ hash_of_your_attributes} .. like below
let(:valid_attributes) {{ first_name: "Virender", last_name: "Sehwag", gender: "Male"}
}
Ahora pasaremos muchos ejemplos de este archivo.
Para invalid_attributes asegúrese de agregar las validaciones en cualquiera de los campos y
let(:invalid_attributes) {{first_name: "br"}
}
En el modelo de usuarios ... la validación de first_name es como =>
validates :first_name, length: {minimum: 5}, allow_blank: true
Ahora todos los ejemplos creados por los generadores pasarán para este controlador_spec