Вот почему мне не нравятся обратные вызовы ActiveRecord - потому что, если вы не хотите иметь ничего общего с обратным вызовом (потому что, скажем, вы делаете вызов внешней службе внутри обратного вызова), вам все равно придется быть обеспокоенным о заглушении этого. Да, вы можете заглушить методы внутри обратного вызова, но это та же проблема, и на самом деле это немного хуже, потому что теперь вы беспокоитесь о чем-то внутри метода, с которым вы не хотите иметь ничего общего.
Как обычно, здесь есть несколько вариантов.
Одна из опций, которую я часто использовал в прошлом, - добавить условие к вашему обратному вызову, которое по умолчанию отключает его. Таким образом, ваш класс Post может выглядеть так:
class Post
before_save :sync_with_store, :if => :syncing_with_store?
def syncing_with_store?; @syncing_with_store; end
attr_writer :syncing_with_store
def sync_with_store
# make an HTTP request or something
end
end
Теперь, где вы действительно хотите вызвать обратный вызов (возможно, в вашем контроллере или где-то еще), вы можете установить post.syncing_with_store = true
перед тем, как позвонить post.save
.
Недостатком этого подхода является то, что вы (и другие разработчики, работающие с вами) должны иметь в виду, и не совсем очевидно, что вы должны это делать. С другой стороны, если вы забудете это сделать, ничего плохого не случится.
Другой вариант - использовать поддельный класс. Скажем, у вас есть сообщение, которое отправляет свои данные во внешнее хранилище данных при сохранении. Вы можете извлечь код, выполняющий push-запрос, в отдельный класс (например, Pusher), который будет доступен в Post.pusher_service
. По умолчанию, однако, это было бы установлено на ложный класс Pusher, который отвечает на тот же интерфейс, но ничего не делает. Так как:
class Post
class << self
attr_accessor :pusher_service
end
self.pusher_service = FakePostPusher
before_save :sync_with_store
def sync_with_store
self.class.pusher_service.run(self)
end
end
class FakePostPusher
def self.run(post)
new(post).run
end
def initialize(post)
@post = post
end
def run
# do nothing
end
end
class PostPusher < FakePostPusher
def run
# actually make the HTTP request or whatever
end
end
В вашем файле рабочей среды вы должны установить Post.pusher_service = Pusher
. В отдельных тестах или тестовых примерах вы создаете подкласс Post - let(:klass) { Class.new(Post) }
- и устанавливаете klass.pusher_service = Pusher
(таким образом, вы не устанавливаете его постоянно и не влияете на будущие тесты).
Третий подход, над которым я экспериментировал, заключается в следующем: просто не используйте обратные вызовы ActiveRecord. Это то, что я взял из скринкастов Гэри Бернхардта (которые, кстати, довольно удивительны). Вместо этого определите класс обслуживания, который оборачивает процесс создания сообщения. Что-то вроде:
class PostCreator
def self.run(attrs={})
new(attrs).run
end
def initialize(attrs={})
@post = Post.new(attrs)
end
def run
if @post.save
make_http_request
return true
else
return false
end
end
def make_http_request
# ...
end
end
Таким образом, PostCreator.run(attrs)
является де-факто способом создания поста вместо прохождения через Пост. Теперь для проверки сохранений в Post нет необходимости отключать обратные вызовы. Если вы хотите протестировать процесс PostCreator, никакой магии не происходит, вы можете легко заглушить любые методы или протестировать их независимо. (Вы можете утверждать, что уничтожение методов здесь - это то же самое, что и блокирование обратных вызовов AR, но я думаю, что это более ясно, что происходит.) Очевидно, что это обрабатывает только пост-создание, но вы могли бы сделать то же самое и для пост-обновления.
Во всяком случае, разные идеи, выберите свой яд.