RSpec change匹配器仅块语法生效 非块写法测试不通过
RSpec change匹配器语法差异导致测试失败问题
问题表现
以下测试写法执行失败:
it { expect { subject }.to change(tt.child_task_transactions.reload, :size).by 1 }
返回错误信息:
expected `TaskTransaction::ActiveRecord_Associations_CollectionProxy#size` to have changed by 1, but was changed by 0
但以下两种写法测试均可正常通过:
- 写法1:先执行subject,再直接断言结果值
it do subject expect(tt.child_task_transactions.reload.size).to eq 1 end
- 写法2:使用块语法的change匹配器
it { expect { subject }.to change { tt.child_task_transactions.reload.size }.by 1 }
当前使用技术栈版本:rspec 3.10.0、Rails 6.1.4.1、Ruby 2.7.2。
核心原因
两种change语法不能随意互换,本质差异是代码的求值时机不同:
change(接收对象, :方法名)形式的匹配器,会在执行subject块之前,就先把括号内的接收对象计算完成并缓存引用。等subject执行结束后,RSpec直接在这个提前缓存的旧对象上调用指定方法,对比前后两次返回值的差值。
失败用例中tt.child_task_transactions.reload在匹配器初始化阶段就已经执行完成,拿到的是subject运行前的关联集合代理对象。等subject执行完新增了关联记录后,RSpec仍然在这个旧的集合代理对象上调用size方法,不会重新触发reload逻辑,自然读不到新关联的记录,最终计算出的变化值为0。change { 表达式 }块形式的匹配器,会分别在subject执行前、执行后两次完整运行块内的所有代码。也就是说两次求值都会完整执行tt.child_task_transactions.reload.size逻辑,每次都会重新从数据库加载最新的关联数据,因此可以正确计算出size的变化值。- 手动先执行
subject再断言的写法,reload逻辑是在subject执行完成后才触发的,直接拿到的就是最新的关联数据,因此断言也能正常通过。
修复建议
涉及ActiveRecord关联查询、需要重新读库或触发reload的场景,统一使用块形式的change匹配器即可,不要把需要动态重新执行的查询逻辑写在change的参数位置,避免被提前求值缓存导致结果不符合预期。
内容的提问来源于stack exchange,提问作者Randomtheories
相关产品推荐
相关产品推荐

