ruby on rails - prueba rspec has_many: through y after_save
ruby-on-rails has-many-through (3)
Supongo que necesitas volver a cargar tu instancia Thing
haciendo @thing.reload
(estoy seguro de que hay una forma de evitar esto, pero eso puede hacer que tu prueba pase al principio y luego puedes descubrir dónde te equivocaste )
Pocas preguntas:
No veo que llames a @thing.save
en tu especificación. ¿Estás haciendo eso, al igual que en el ejemplo de tu consola?
¿Por qué llamas a t.save
y no a tu u.save
en tu prueba de consola, teniendo en cuenta que estás empujando hacia ti? Al guardarlo, deberías disparar un save to t
, obteniendo el resultado final que deseas, y creo que "tendrá más sentido" teniendo en cuenta que realmente trabajas en ti, no en t
.
Tengo una relación (creo) bastante directa has_many :through
con una tabla join:
class User < ActiveRecord::Base
has_many :user_following_thing_relationships
has_many :things, :through => :user_following_thing_relationships
end
class Thing < ActiveRecord::Base
has_many :user_following_thing_relationships
has_many :followers, :through => :user_following_thing_relationships, :source => :user
end
class UserFollowingThingRelationship < ActiveRecord::Base
belongs_to :thing
belongs_to :user
end
Y estas pruebas de rspec (sé que estas no son necesariamente buenas pruebas, estas son solo para ilustrar lo que está sucediendo):
describe Thing do
before(:each) do
@user = User.create!(:name => "Fred")
@thing = Thing.create!(:name => "Foo")
@user.things << @thing
end
it "should have created a relationship" do
UserFollowingThingRelationship.first.user.should == @user
UserFollowingThingRelationship.first.thing.should == @thing
end
it "should have followers" do
@thing.followers.should == [@user]
end
end
Esto funciona bien HASTA que agregue un after_save
al modelo Thing
que hace referencia a sus followers
. Es decir, si lo hago
class Thing < ActiveRecord::Base
after_save :do_stuff
has_many :user_following_thing_relationships
has_many :followers, :through => :user_following_thing_relationships, :source => :user
def do_stuff
followers.each { |f| puts "I''m followed by #{f.name}" }
end
end
Luego, la segunda prueba falla, es decir, la relación se sigue agregando a la tabla de unión, pero @thing.followers
devuelve una matriz vacía. Además, esa parte de la devolución de llamada nunca se llama (como si los followers
estuvieran vacíos dentro del modelo). Si agrego un puts "HI"
en la devolución de llamada antes de los followers.each
línea, el "HI" aparece en stdout, así que sé que se está llamando a la devolución de llamada. Si hago un comentario sobre los followers.each
línea, las pruebas vuelven a pasar.
Si hago esto a través de la consola, funciona bien. Es decir, puedo hacer
>> t = Thing.create!(:name => "Foo")
>> t.followers # []
>> u = User.create!(:name => "Bar")
>> u.things << t
>> t.followers # [u]
>> t.save # just to be super duper sure that the callback is triggered
>> t.followers # still [u]
¿Por qué está fallando en rspec? ¿Estoy haciendo algo horriblemente mal?
Actualizar
Todo funciona si defino manualmente Thing#followers
como
def followers
user_following_thing_relationships.all.map{ |r| r.user }
end
Esto me lleva a creer que quizás estoy definiendo mi has_many :through
de :source
incorrectamente?
Actualizar
Creé un proyecto de ejemplo mínimo y lo puse en github: https://github.com/dantswain/RspecHasMany
Otra actualización
Muchas gracias a @PeterNixey y @kikuchiyo por sus sugerencias a continuación. La respuesta final resultó ser una combinación de ambas respuestas y desearía poder dividir el crédito entre ellas. He actualizado el proyecto github con lo que creo que es la solución más limpia e impulsé los cambios: https://github.com/dantswain/RspecHasMany
Todavía me encantaría que alguien pudiera darme una explicación realmente sólida de lo que está sucediendo aquí. Lo más inquietante para mí es por qué, en la declaración inicial del problema, todo (excepto la operación de la devolución de llamada en sí) funcionaría si comente la referencia a los followers
.
RESPUESTA ACTUALIZADA ** Esto pasa rspec, sin stubbing, ejecutando callbacks para guardar (after_save callback incluido), y comprueba que @ thing.followers no esté vacío antes de intentar acceder a sus elementos. (;
describe Thing do
before :each do
@user = User.create(:name => "Fred");
@thing = Thing.new(:name => ''Foo'')
@user.things << @thing
@thing.run_callbacks(:save)
end
it "should have created a relationship" do
@thing.followers.should == [@user]
puts @thing.followers.inspect
end
end
class Thing < ActiveRecord::Base
after_save :some_function
has_many :user_following_thing_relationships
has_many :followers, :through => :user_following_thing_relationships, :source => :user
def some_function
the_followers = followers
unless the_followers.empty?
puts "accessing followers here: the_followers = #{the_followers.inspect}..."
end
end
end
RESPUESTA ORIGINAL **
Pude hacer que las cosas funcionaran con la devolución de llamada after_save, siempre que no hiciera referencia a los followers
dentro del cuerpo / bloque de do_stuff
. ¿Tiene que hacer referencia a los followers
en el método real al que llama desde after_save
?
Código actualizado para anular la devolución de llamada. Ahora el modelo puede permanecer como lo necesita, mostramos @ thing.followers de hecho está configurado como esperábamos, y podemos investigar la funcionalidad de do_stuff / some_function a través de after_save en una especificación diferente.
Empujé una copia del código aquí: https://github.com/kikuchiyo/RspecHasMany
Y el código de spec thing passing está a continuación:
# thing_spec.rb
require ''spec_helper''
describe Thing do
before :each do
Thing.any_instance.stub(:some_function) { puts ''stubbed out...'' }
Thing.any_instance.should_receive(:some_function).once
@thing = Thing.create(:name => "Foo");
@user = User.create(:name => "Fred");
@user.things << @thing
end
it "should have created a relationship" do
@thing.followers.should == [@user]
puts @thing.followers.inspect
end
end
# thing.rb
class Thing < ActiveRecord::Base
after_save :some_function
has_many :user_following_thing_relationships
has_many :followers, :through => :user_following_thing_relationships, :source => :user
def some_function
# well, lets me do this, but I cannot use @x without breaking the spec...
@x = followers
puts ''testing puts hear shows up in standard output''
x ||= 1
puts "testing variable setting and getting here: #{x} == 1/n/t also shows up in standard output"
begin
# If no stubbing, this causes rspec to fail...
puts "accessing followers here: @x = #{@x.inspect}..."
rescue
puts "and this is but this is never seen."
end
end
end
Tuve problemas similares en el pasado que se resolvieron recargando la asociación (en lugar del objeto principal).
¿ thing.followers
si thing.followers
en el RSpec?
it "should have followers" do
@thing.followers.reload
@thing.followers.should == [@user]
end
EDITAR
Si (como usted menciona) tiene problemas con las devoluciones de llamada que no son despedidas, entonces puede hacer esta recarga en el objeto mismo:
class Thing < ActiveRecord::Base
after_save { followers.reload}
after_save :do_stuff
...
end
o
class Thing < ActiveRecord::Base
...
def do_stuff
followers.reload
...
end
end
No sé por qué RSpec tiene problemas para no volver a cargar las asociaciones, pero me he topado con los mismos tipos de problemas
Editar 2
Aunque @dantswain confirmó que la carga de followers.reload
ayudó a aliviar algunos de los problemas, aún no los solucionó a todos.
Para hacer eso, la solución necesitaba una solución de @kikuchiyo que requería una llamada de save
después de hacer las devoluciones de llamada en Thing
:
describe Thing do
before :each do
...
@user.things << @thing
@thing.run_callbacks(:save)
end
...
end
Sugerencia final
Creo que esto está sucediendo por el uso de <<
en una operación has_many_through
. No veo que el <<
de hecho active su evento after_save
en absoluto:
Tu código actual es este:
describe Thing do
before(:each) do
@user = User.create!(:name => "Fred")
@thing = Thing.create!(:name => "Foo")
@user.things << @thing
end
end
class Thing < ActiveRecord::Base
after_save :do_stuff
...
def do_stuff
followers.each { |f| puts "I''m followed by #{f.name}" }
end
end
y el problema es que do_stuff
no se llama. Creo que este es el comportamiento correcto sin embargo.
Repasemos el RSpec:
describe Thing do
before(:each) do
@user = User.create!(:name => "Fred")
# user is created and saved
@thing = Thing.create!(:name => "Foo")
# thing is created and saved
@user.things << @thing
# user_thing_relationship is created and saved
# no call is made to @user.save since nothing is updated on the user
end
end
El problema es que el tercer paso en realidad no requiere que el objeto thing
se vuelva a guardar, simplemente crear una entrada en la tabla de unión.
Si quieres asegurarte de que @user llama a guardar, probablemente puedas obtener el efecto que deseas así:
describe Thing do
before(:each) do
@thing = Thing.create!(:name => "Foo")
# thing is created and saved
@user = User.create!(:name => "Fred")
# user is created BUT NOT SAVED
@user.things << @thing
# user_thing_relationship is created and saved
# @user.save is also called as part of the addition
end
end
También puede encontrar que la after_save
llamada after_save
está, de hecho, en el objeto equivocado y que preferiría tenerlo en el objeto de relación. Finalmente, si la devolución de llamada realmente pertenece al usuario y usted necesita que se active después de crear la relación, puede usar la touch
para actualizar al usuario cuando se crea una nueva relación.