这就是我不喜欢 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
.
这种方法的缺点是,您(以及与您一起工作的其他开发人员)必须牢记这一点,而且您必须这样做并不是很明显。另一方面,如果您忘记这样做,则不会发生任何不好的事情。
另一种选择是使用假类。假设您有一个 Post,它在保存时将其数据推送到外部数据存储。您可以提取执行推送到单独的类(例如 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 回调。这是我从Gary Bernhardt 的截屏视频中获得的(顺便说一句,这非常棒)。相反,定义一个服务类来包装创建帖子的行为。就像是:
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 的方式。现在要在 Post 中测试保存,无需存根回调。如果您想测试 PostCreator 过程,没有什么神奇的,您可以轻松地删除您想要的任何方法或独立测试它们。(你可能会争辩说,在这里存根方法与存根 AR 回调是一样的,但我认为它更明确地发生了什么事。)显然这只处理帖子创建,但你也可以对帖子更新做同样的事情。
反正想法不一样,挑你的毒。