英文:
RSpec allow persisting between contexts
问题
我已经翻译了你提供的内容。请注意,我只翻译文本,不包括代码部分。
我有一个作为单例对象运行的Ruby模块,它充当我的更广泛应用程序与持久化数据的文件系统JSON对象之间的接口。
我正在编写用于测试单例的唯一公共方法行为的规范,注意到围绕rspec(3.12)的`allow`存在奇怪的行为。
我试图测试单例在特定状态的情况下执行的操作(例如,传递给它的参数没有特定属性),这可能会发生在方法进行一半时。
我正在模拟单例的所有下游行为,这些行为可能引发错误或提示调用`gets`,以隔离特定状态下的行为并进行成功的测试。这意味着使用给定参数存根`File#read`。
我的问题是,我有两个同样有效的状态要测试,而文件的允许存根行为在声明它的上下文结束后仍然存在。
此时,直接查看代码可能更有用。
我的单例:
production.rb
frozen_string_literal: true
require 'json'
module AtomicCards
module Mana
# 定义土地面可以敲击的法术颜色
module Production
extend self
LOCAL_DATA_PATH = 'lib/mtg_json/data'
LOCAL_FILE_NAME = 'mana_production'
FileUtils.mkpath(LOCAL_DATA_PATH)
def from(card_face)
return unless card_face['types'].include?('Land')
face_name = card_face['faceName'] || card_face['name']
raise "no face_name on #{card_face}" unless face_name
cache_user_provided_production(face_name, card_face['type'], card_face['text']) unless cache[face_name]
cache[face_name]
end
private
def cache
@cache ||= local_file_available? ? local_data : {}
end
def complete_local_path = "#{LOCAL_DATA_PATH}/#{LOCAL_FILE_NAME}.json"
def local_file_available? = File.size?(complete_local_path)
def local_data = JSON.parse(File.read(complete_local_path))
def cache_user_provided_production(face_name, ...)
# system('clear')
validated_input = validated_user_input(face_name, ...)
cache.merge!({ face_name => %w[W U B R G C] & validated_input.chars }) && save!
# system('clear')
end
def validated_user_input(...)
prompt_user(...)
case input = gets.chomp.upcase!
when 'NIL' then input.clear
when /^[WUBRGC]*$/ then input
else retry_user_input(...)
end
end
def prompt_user(face_name, type_line, text_box)
puts "Please enter mana production string (wubrgc) or 'nil' for #{face_name}:"
puts
puts type_line
puts text_box
end
def retry_user_input(...)
system('clear')
puts 'Invalid input detected. Try again.'
validated_user_input(...)
end
def save!
File.binwrite(CACHE_FILE_PATH, JSON.pretty_generate(cache.sort.to_h))
end
end
end
end
我的规范:
production_spec.rb
frozen_string_literal: true
require 'rails_helper'
require '/app/lib/mtg_json/atomic_cards/mana/production'
Production = AtomicCards::Mana::Production
RSpec.describe Production do
subject { Production }
let(:mocked_non_land) { { 'types' => %w[Planeswalker] } }
let(:mocked_nameless_land_face) { { 'types' => %w[Land] } }
let(:mocked_single_face_land) { { 'name' => 'Fake Single-Faced Land', 'types' => %w[Land] } }
let(:mocked_dual_face_land) { { 'faceName' => 'Fake Dual-Faced Land', 'types' => %w[Land] } }
let(:mocked_complete_cache) { { 'Fake Single-Faced Land' => [], 'Fake Dual-Faced Land' => [] }.to_json }
before(:each) do
allow(File).to receive(:read).and_call_original
allow(File).to receive(:size?).and_return(false)
end
describe '.from' do
context 'when passed a non-land card face' do
specify { expect(subject.from(mocked_non_land)).to be_nil }
end
context 'when passed a card face without a name-like attribute' do
specify { expect { subject.from(mocked_nameless_land_face) }.to raise_error(RuntimeError) }
end
context 'when passed a card face with a name attribute' do
specify do
allow(File).to receive(:size?).and_return(true)
allow(File).to receive(:read).and_return(mocked_complete_cache)
expect { subject.from(mocked_single_face_land) }.not_to raise_error
end
end
context 'when passed a card face with a faceName attribute' do
specify do
# allow(File).to receive(:size?).and_return(true)
# allow(File).to receive(:read).and_return(mocked_complete_cache)
expect { subject.from(mocked_dual_face_land) }.not_to raise_error
end
end
context 'when a local cache file exists' do
end
context 'when no local cache file exists' do
end
context 'when the face name does not appear in the cache' do
end
context 'when the face name appears in the cache' do
end
end
end
上面的规范运行通过了所有四个测试,尽管“当传递具有faceName属性的卡牌面”应该失败,因为`File`在这里不应该表现出存根行为。
事实上,如果将测试3的两行`allow`注释掉,rspec运行将按预期完成失败。
我已经尝试通过将测试3的`allow`行包装在before块中、调用链式`.with(filename)`方法等方法来使上面的rspec失败,但都无济于事。
我是否对RSpec的`allow`方法的作用域规则有所误解?
<details>
<summary>英文:</summary>
I have a Ruby (`3.1.0p0 (2021-12-25 revision fb4df44d16) [x86_64-linux]`) module functioning as a singleton object that acts as an interface between my broader application and a file system json object that persists data.
I am part of the way through writing specs to test the behavior of the singleton's sole public method, and am noticing odd behavior surrounding rspec's (`3.12`) `allow`.
I am trying to test what the singleton does in the context of a specific state (eg the argument passed to it does not have a specific attribute) which might occur halfway through the method.
I am stubbing out all downstream behaviors of the singleton that might raise errors or prompt calls to `gets` to isolate the behavior in the context of the state and have a successful test. This means stubbing `File#read` with a given argument.
My problem is that I have two equally valid states to test and the allowance of File to have the stubbed behavior is persisting once the context in which is declared ends.
At this point, looking at the code directly is probably more useful.
my singleton:
production.rb
frozen_string_literal: true
require 'json'
module AtomicCards
module Mana
# Defines the colors of mana for which a land face can tap
module Production
extend self
LOCAL_DATA_PATH = 'lib/mtg_json/data'
LOCAL_FILE_NAME = 'mana_production'
FileUtils.mkpath(LOCAL_DATA_PATH)
def from(card_face)
return unless card_face['types'].include?('Land')
face_name = card_face['faceName'] || card_face['name']
raise "no face_name on #{card_face}" unless face_name
cache_user_provided_production(face_name, card_face['type'], card_face['text']) unless cache[face_name]
cache[face_name]
end
private
def cache
@cache ||= local_file_available? ? local_data : {}
end
def complete_local_path = "#{LOCAL_DATA_PATH}/#{LOCAL_FILE_NAME}.json"
def local_file_available? = File.size?(complete_local_path)
def local_data = JSON.parse(File.read(complete_local_path))
def cache_user_provided_production(face_name, ...)
# system('clear')
validated_input = validated_user_input(face_name, ...)
cache.merge!({ face_name => %w[W U B R G C] & validated_input.chars }) && save!
# system('clear')
end
def validated_user_input(...)
prompt_user(...)
case input = gets.chomp.upcase!
when 'NIL' then input.clear
when /^[WUBRGC]*$/ then input
else retry_user_input(...)
end
end
def prompt_user(face_name, type_line, text_box)
puts "Please enter mana production string (wubrgc) or 'nil' for #{face_name}:"
puts
puts type_line
puts text_box
end
def retry_user_input(...)
system('clear')
puts 'Invalid input detected. Try again.'
validated_user_input(...)
end
def save!
File.binwrite(CACHE_FILE_PATH, JSON.pretty_generate(cache.sort.to_h))
end
end
end
end
my spec:
production_spec.rb
frozen_string_literal: true
require 'rails_helper'
require '/app/lib/mtg_json/atomic_cards/mana/production'
Production = AtomicCards::Mana::Production
RSpec.describe Production do
subject { Production }
let(:mocked_non_land) { { 'types' => %w[Planeswalker] } }
let(:mocked_nameless_land_face) { { 'types' => %w[Land] } }
let(:mocked_single_face_land) { { 'name' => 'Fake Single-Faced Land', 'types' => %w[Land] } }
let(:mocked_dual_face_land) { { 'faceName' => 'Fake Dual-Faced Land', 'types' => %w[Land] } }
let(:mocked_complete_cache) { { 'Fake Single-Faced Land' => [], 'Fake Dual-Faced Land' => [] }.to_json }
before(:each) do
allow(File).to receive(:read).and_call_original
allow(File).to receive(:size?).and_return(false)
end
describe '.from' do
context 'when passed a non-land card face' do
specify { expect(subject.from(mocked_non_land)).to be_nil }
end
context 'when passed a card face without a name-like attribute' do
specify { expect { subject.from(mocked_nameless_land_face) }.to raise_error(RuntimeError) }
end
context 'when passed a card face with a name attribute' do
specify do
allow(File).to receive(:size?).and_return(true)
allow(File).to receive(:read).and_return(mocked_complete_cache)
expect { subject.from(mocked_single_face_land) }.not_to raise_error
end
end
context 'when passed a card face with a faceName attribute' do
specify do
# allow(File).to receive(:size?).and_return(true)
# allow(File).to receive(:read).and_return(mocked_complete_cache)
expect { subject.from(mocked_dual_face_land) }.not_to raise_error
end
end
context 'when a local cache file exists' do
end
context 'when no local cache file exists' do
end
context 'when the face name does not appear in the cache' do
end
context 'when the face name appears in the cache' do
end
end
end
Running the spec above passes all four tests, even though `when passed a card face with a faceName attribute` ought to fail, since `File` should not exhibit stubbed behavior here.
Indeed, if the two `allow` lines from `when passed a card face with a name attribute` (test 3) are commented out, the rspec run completes with failures as expected.
I have tried making the rspec as copied above fail by wrapping test 3's `allow` lines in a before block, calling a daisychained `.with(filename)` method, among other things to no avail.
Is there something I am not understanding about the scoping rules of RSpec's `allow` method?
</details>
# 答案1
**得分**: 1
[Konstantin Strukov](https://stackoverflow.com/users/8008340/konstantin-strukov) 强调了问题的关键:我正在对我的单例变量 `@cache` 进行记忆化处理。
由于这个值在 Ruby 进程的生命周期内保持不变,因此有两种解决方案:
1) 放弃记忆化,寻找单例行为的不同编码策略
2) 在适当的位置添加 `Production.remove_instance_variable(:@cache) if Production.instance_variable_defined?(:@cache)` 到 `before(:each)` 块中,以在期望/上下文之间重置该值
我计划在 `db:seed` 过程中多次运行此单例的 `.from` 方法,因此需要使用上述的解决方案 2)。
非常感谢,Konstantin!
<details>
<summary>英文:</summary>
[Konstantin Strukov](https://stackoverflow.com/users/8008340/konstantin-strukov) nailed the issue on its head: I am memoizing the `@cache` instance variable on my singleton.
As this value persists for the duration of the Ruby process, the two solutions are:
1) abandon the memoization, finding a different coding strategy for my singleton's behavior
2) add `Production.remove_instance_variable(:@cache) if Production.instance_variable_defined?(:@cache)` to an appropriately-placed `before(:each)` block to reset the value between expectations/contexts
I plan on having this singleton's `.from` methods run several thousand times during my `db:seed` procedure, necessitating solution 2) above.
Many thanks, Konstantin!
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论