0

The code:

class Weird
  DEFAULT_FOO = 'fo0'

  attr_accessor :foo

  def initialize(val)
    @foo = val
  end

  def run
    unless foo
      foo = DEFAULT_FOO         # Fails
      # @foo = DEFAULT_FOO      # Works
    end

    bar(foo)
  end

  def bar(f)
    puts "The foo is '#{f}'"
  end
end

RSpec.describe Weird do
  subject(:weird) do
    described_class.new(foo_val)
  end
  let(:foo_val) { nil }

  describe '#run' do
    context 'without foo' do
      it 'bars the foo' do
        expect(subject).to receive(:bar).with(described_class::DEFAULT_FOO)
        subject.run
      end
    end

    context 'with foo' do
      let(:foo_val) { 'quux' }

      it 'bars the foo' do
        expect(subject).to receive(:bar).with(foo_val)
        subject.run
      end
    end
  end
end

Run this with RSpec (rspec weird.rb) and the second test fails with:

  1) Weird#run with foo bars the foo
     Failure/Error: bar(foo)

       #<Weird:0x007fc1948727f8 @foo="quux"> received :bar with unexpected arguments
         expected: ("quux")
              got: (nil)
       Diff:
       @@ -1,2 +1,2 @@
       -["quux"]
       +[nil]

     # ./test_case_2.rb:21:in `run'
     # ./test_case_2.rb:48:in `block (4 levels) in <top (required)>'

The implicitly defined foo=() attribute writer method is not being called; explicitly defining one does not work either. Instead, it seems a method-local variable foo is being created (and set) where it shouldn't be. Why is this happening?

Ruby version is ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin15].

scry
  • 1,237
  • 12
  • 29

2 Answers2

1

foo = DEFAULT_FOO fails because this is creating a local variable foo and assigning it the value DEFAULT_FOO. But when you're calling bar(foo) it will looks for the @ value first, which in this case will still be nil.

When assigning an instance variable you must either use self.foo = or @foo =.

Levsero
  • 591
  • 1
  • 4
  • 14
  • Consider making the `attr_accessor` private, unless you intend to expose it to outside classes. If you need to expose it as read-only, use `attr_reader` for your public method and `attr_writer` as a private method. – moveson Dec 25 '16 at 05:40
  • @moveson. private `attr_writer`, why? Just use `@foo="bar"` – Eric Duminil Dec 25 '16 at 11:06
  • 3
    @EricDuminil: Methods can be overridden, refactored, hooked, aliased. Ivars can't. – Jörg W Mittag Dec 25 '16 at 13:08
  • @Levsero the `foo` attribute is _not_ nil in the second test case. Seems the local variable is being looked up first, but since the `unless` block is not getting run, the local `foo` is declared, but never initialized. – scry Dec 26 '16 at 04:22
0

2 ways to fix

  1. use self.foo = DEFAULT_FOO
  2. use @foo = DEFAULT_FOO
duykhoa
  • 2,227
  • 1
  • 25
  • 43