RSpec允许在不同上下文之间保持不变。

huangapple go评论52阅读模式
英文:

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&#39;s sole public method, and am noticing odd behavior surrounding rspec&#39;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 = &#39;lib/mtg_json/data&#39;
  LOCAL_FILE_NAME = &#39;mana_production&#39;
  FileUtils.mkpath(LOCAL_DATA_PATH)

  def from(card_face)
    return unless card_face[&#39;types&#39;].include?(&#39;Land&#39;)

    face_name = card_face[&#39;faceName&#39;] || card_face[&#39;name&#39;]
    raise &quot;no face_name on #{card_face}&quot; unless face_name

    cache_user_provided_production(face_name, card_face[&#39;type&#39;], card_face[&#39;text&#39;]) unless cache[face_name]

    cache[face_name]
  end

  private

  def cache
    @cache ||= local_file_available? ? local_data : {}
  end

  def complete_local_path = &quot;#{LOCAL_DATA_PATH}/#{LOCAL_FILE_NAME}.json&quot;
  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(&#39;clear&#39;)
    validated_input = validated_user_input(face_name, ...)

    cache.merge!({ face_name =&gt; %w[W U B R G C] &amp; validated_input.chars }) &amp;&amp; save!
    # system(&#39;clear&#39;)
  end

  def validated_user_input(...)
    prompt_user(...)

    case input = gets.chomp.upcase!
    when &#39;NIL&#39; then input.clear
    when /^[WUBRGC]*$/ then input
    else retry_user_input(...)
    end
  end

  def prompt_user(face_name, type_line, text_box)
    puts &quot;Please enter mana production string (wubrgc) or &#39;nil&#39; for #{face_name}:&quot;
    puts
    puts type_line
    puts text_box
  end

  def retry_user_input(...)
    system(&#39;clear&#39;)
    puts &#39;Invalid input detected. Try again.&#39;
    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 &#39;when passed a card face without a name-like attribute&#39; do
  specify { expect { subject.from(mocked_nameless_land_face) }.to raise_error(RuntimeError) }
end

context &#39;when passed a card face with a name attribute&#39; 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 &#39;when passed a card face with a faceName attribute&#39; 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 &#39;when a local cache file exists&#39; do
end

context &#39;when no local cache file exists&#39; do
end

context &#39;when the face name does not appear in the cache&#39; do
end

context &#39;when the face name appears in the cache&#39; 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&#39;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&#39;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&#39;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&#39;s `.from` methods run several thousand times during my `db:seed` procedure, necessitating solution 2) above.

Many thanks, Konstantin!

</details>



huangapple
  • 本文由 发表于 2023年6月15日 00:25:01
  • 转载请务必保留本文链接:https://go.coder-hub.com/76475696.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定