ruby-on-rails ruby rspec has-many-through after-save

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.