你是否曾为你的Rails应用程序中的缓慢测试而苦恼?你知道为什么你的测试很慢吗?好吧,这似乎并不明显,但大部分时间测试都花在与数据库的交互上。我们在这里不证明这个事实,因为它是众所周知的,但我们讨论一个可能的解决方案,在某些情况下可能会有帮助。
问题
如果一个Rails应用程序使用RSpec进行测试,并使用一些工厂(例如FactoryBot)在测试中创建模型。用工厂创建模型的测试很慢。我们想让它们变得更快。
解决方案
有一些关于如何加快测试速度的建议,它们在这篇文章中被明确定义。他们非常有帮助,而且我必须承认,他们工作得很好。但是,如果我们想从我们的测试中获得更多的速度,不管是什么原因呢?正如已经说过的,测试之所以慢是因为与数据库的交互。那么,我们可以做什么呢?这很明显--把调用数据库的代码存根化,然后就可以了。唯一的问题是,我们的应用程序与数据库对话有两个原因:保存数据和读取数据。而这些是完全不同的问题,每个问题都需要特定的方法。在这篇文章中,我们只讨论 "读 "的情况。而 "写 "的情况则留给读者思考。
我们已经知道它们的速度很慢,因为触及了DB。但是,究竟是哪些代码导致了这个问题呢?让我们先想想为什么我们在测试装饰器时需要创建真实的模型。你可能会争论,但在我看来,大多数时候我们需要持久化的模型来满足一些来自DB的选择,这实际上是Rails模型层的作用域或关联。那么,为什么不把这些东西存根化呢?
好吧,我们知道了我们需要在装饰器的测试中存根关联和作用域。但具体怎么做呢?假设,在我们的模型深处有一段代码被我们的装饰器调用:我们测试SubscriptionDecorator.new(subscription).active_users ,实际上变成了在下面调用subscription.users.active.verified 。像这样的存根allow(subscription).to receive(users).and_return([User.new]) 不会起作用,因为在它上面没有定义的active 或verified 方法。所以,看起来我们需要明智地存根。这就是我的建议。让我们创建一个克隆的ActiveRecord::Relation ,复制一个关联行为:
class ActiveRecordRelationStub
attr_reader :records
alias to_a records
# @param model_klass [ActiveRecord::Base] the stubbing association's class
# @param records [Array] list of records the association holds
# @param scopes [Array] list of stubbed scopes
def initialize(model_klass, records, scopes: [])
@records = records
scopes.each do |scope|
fail NotImplementedError, scope unless model_klass.respond_to?(scope)
define_singleton_method(scope) do
self
end
end
end
end
有了这个,我们就可以存根我们的关联和范围:
user1 = build_stubbed(:user)
user2 = build_stubbed(:user)
allow(subscription).to receive(:users).and_return(ActiveRecordRelationStub.new(User, [user1, user2], scopes: [:active, :verified]))
为了演示它是如何工作的,为了简单起见,让我们的SubscriptionDecorator 类以如下方式定义:
class SubscriptionDecorator
# ... details are hidden here
def active_users
subscription.users.active.verified
end
end
现在,当在测试中调用SubscriptionDecorator.new(subscription).active_users 中的SubscriptionDecorator#active_users 时,返回带有所提供的记录的存根关系,这与真实模型的行为完全一样。但是请记住,在这里我们并不关心作用域(verified 或active )是如何定义的内部细节,它们使用什么过滤器等等。作用域的真实行为应该在它们被定义的模型中测试。换句话说,在装饰器层,我们依赖于作用域的实现,并假定它们工作正确且测试良好,因此我们可以安全地存根它们。请注意,为了防止错误地存根于未定义的关联,最好使用验证性的部分替身。如果使用得当,RSpec会检查被存根的方法是否真的被定义了,如果不是,就会引发一个异常。因此,你会被告知这个错别字或错误,并可以修复它,而不是等待在生产中发生的错误,因为你依赖的测试很弱。我强烈建议你为你的测试全面打开这个选项,以确保安全。
总结
我曾经使用过这篇文章中描述的技术,而且非常成功。我设法使我们的测试变得更快。当然,这种方法不仅可以用在装饰器的测试中,还可以用在视图规格、控制器规格和任何地方,当你并不真正需要检查DB是否正常工作,而是想检查你的代码。但这并不是故事的全部。还应该有定义好的聚合函数,一些过滤器方法等等。如果这种方法对你也有效,我愿意评论一下这里缺少什么功能。