rspec-тестирование has_many: through и after_save - PullRequest
5 голосов
/ 18 января 2012

У меня есть (я думаю) относительно простые has_many :through отношения с таблицей соединений:

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

И эти тесты rspec (я знаю, что это не обязательно хорошие тесты, они просто для иллюстрации того, чтопроисходит):

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

Это прекрасно работает, пока я не добавлю after_save к модели Thing, которая ссылается на followers.То есть, если я сделаю

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

, тогда второй тест не пройден - то есть отношение все еще добавляется в таблицу соединений, но @thing.followers возвращает пустой массив.Кроме того, эта часть обратного вызова никогда не вызывается (как будто followers пусто в модели).Если я добавлю puts "HI" в обратном вызове перед строкой followers.each, на стандартном выводе отобразится «HI», поэтому я знаю, что вызывается обратный вызов.Если я закомментирую строку followers.each, то тесты снова пройдут.

Если я делаю это через консоль, все работает нормально.То есть я могу сделать

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

Почему это не работает в rspec?Я делаю что-то ужасно неправильно?

Обновление

Все работает, если я вручную определяю Thing#followers как

def followers
  user_following_thing_relationships.all.map{ |r| r.user }
end

Это заставляет меня поверитьчто, возможно, я неправильно определяю has_many :through с :source?

Обновление

Я создал минимальный пример проекта и поместил его на github: https://github.com/dantswain/RspecHasMany

Еще одно обновление

Огромное спасибо @PeterNixey и @kikuchiyo за их предложения ниже.Окончательный ответ оказался комбинацией обоих ответов, и я хотел бы разделить кредит между ними.Я обновил проект github тем, что я считаю самым чистым решением, и внес изменения: https://github.com/dantswain/RspecHasMany

Мне бы все равно понравилось, если бы кто-то мог дать мне действительно убедительное объяснение того, что здесь происходит.Самым неприятным моментом для меня является то, почему в первоначальной постановке задачи все (кроме операции самого обратного вызова) будет работать, если я закомментирую ссылку на followers.

Ответы [ 3 ]

8 голосов
/ 22 января 2012

В прошлом у меня были похожие проблемы, которые решались путем перезагрузки ассоциации (а не родительского объекта).

Работает ли это, если вы перезагрузите thing.followers в RSpec?

it "should have followers" do
  @thing.followers.reload
  @thing.followers.should == [@user]
end 

РЕДАКТИРОВАТЬ

Если (как вы упомянули) у вас проблемы с обратными вызовами, которые не запускаются, вы можете выполнить перезагрузку в самом объекте:

class Thing < ActiveRecord::Base
  after_save { followers.reload}
  after_save :do_stuff
  ...
end

или

class Thing < ActiveRecord::Base
  ...
  def do_stuff
    followers.reload
    ...
  end
end

Я не знаю, почему у RSpec есть проблемы с не перезагрузкой ассоциаций, но я сам сталкивался с такими же проблемами

Редактировать2

Несмотря на то, что @dantswain подтвердил, что followers.reload помог решить некоторые проблемы, он все еще не решил их все.

Для этого решению потребовалось исправление от @kikuchiyo, которое требовало вызова save после выполнения обратных вызовов в Thing:

describe Thing do
  before :each do
    ...
    @user.things << @thing
    @thing.run_callbacks(:save)
  end 
  ...
end

Окончательное предложение

Я полагаю, что это происходит из-за использования << в операции has_many_through.Я не вижу, чтобы << фактически вообще вызывал ваше after_save событие:

Ваш текущий код такой:

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

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

Давайте пройдемся по 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

Проблема состоит в том, что третий шаг фактически не требует восстановления объекта thing - это просто создание записи в таблице соединения,

Если вы хотите убедиться, что @user делает вызов save, вы, вероятно, можете получить желаемый эффект, подобный следующему:

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

Вы также можете обнаружить, что after_saveфактически обратный вызов находится на неправильном объекте, и вы бы предпочли иметь его вместо объекта отношения.Наконец, если обратный вызов действительно принадлежит пользователю и вам нужно его запустить после создания отношения, вы можете использовать touch для обновления пользователя при создании нового отношения.

2 голосов
/ 22 января 2012

ОБНОВЛЕННЫЙ ОТВЕТ ** Передача rspec без заглушек, запуск обратных вызовов для сохранения (включая обратный вызов after_save) и проверка того, что @ thing.followers не пусто, прежде чем пытаться получить доступего элементы.(;

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

ОРИГИНАЛЬНЫЙ ОТВЕТ **

Мне удалось заставить вещи работать с обратным вызовом after_save, пока яне ссылался на followers в теле / ​​блоке do_stuff. Нужно ли ссылаться на followers в реальном методе, который вы вызываете из after_save?

Обновлен код для блокировки обратного вызова.модель может оставаться так, как вам нужно, мы показываем, что @ thing.followers действительно установлен так, как мы ожидали, и мы можем исследовать функциональность do_stuff / some_function через after_save в другой спецификации.

Я нажал копиюкод здесь: https://github.com/kikuchiyo/RspecHasMany

и предмет прохождения спецификации * код ниже:

# 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
1 голос
/ 18 января 2012

Я предполагаю, что вам нужно перезагрузить свой экземпляр Thing, выполнив @thing.reload (я уверен, что есть способ избежать этого, но это может сначала пройти ваш тест, а затем вы сможете выяснить, где выЯ ошибся).

Несколько вопросов:

Я не вижу, чтобы вы звонили @thing.save в вашей спецификации.Вы делаете это, как в примере с вашей консолью?

Почему вы вызываете t.save, а не u.save в тесте консоли, учитывая, что вы нажимаете t на u?Сохранение u должно привести к сохранению до t, получая желаемый конечный результат, и я думаю, что это "будет иметь больше смысла", учитывая, что вы действительно работаете с u, а не t.

...