Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redefine a class constant #13

Closed
fguillen opened this issue Nov 1, 2010 · 10 comments

Comments

@fguillen
Copy link

commented Nov 1, 2010

I would like to mock a class constant but it does not work:

require 'test/unit'
require 'mocha'

class A
  MY_CONST = 'a'
end

class MyTest < Test::Unit::TestCase
  def test_should_returns_b
    assert_equal( 'a', A::MY_CONST )
    A.stubs( :MY_CONST ).returns( 'b' )
    assert_equal( 'b', A::MY_CONST )
  end
end

Is there any way to do something like this?

Thanks.

f.

@floehopper

This comment has been minimized.

Copy link
Member

commented Nov 1, 2010

Mocha is only designed to work with methods, not constants.

I can think of at least 3 options :-

  1. Update the referenced String in-place (constants are only constant references)

    def test_should_returns_b
      assert_equal('a', A::MY_CONST)
      A::MY_CONST.gsub!(/a/, 'b')
      assert_equal('b', A::MY_CONST)
    end
    
  2. Remove and set the constant on the class (removal avoids a warning when setting) :-

    def test_should_returns_b
      assert_equal('a', A::MY_CONST)
      A.send(:remove_const, :MY_CONST)
      A.const_set(:MY_CONST, 'b')
      assert_equal('b', A::MY_CONST)
    end
    
  3. Wrap the constant in a method (if it changes in a test, is it really a constant?) :-

    class A
      MY_CONST = 'a'
      def self.my_const; MY_CONST; end
    end
    
    class MyTest < Test::Unit::TestCase
      def test_should_returns_b
        assert_equal('a', A.my_const)
        A.stubs(:my_const).returns('b')
        assert_equal('b', A.my_const)
      end
    end
    

You might also want to consider whether you need to stub the constant at all. It might be sufficient (and more intention revealing) to reference the constant in your tests.

I hope that helps. Please close this issue if you are happy with the response.

@fguillen

This comment has been minimized.

Copy link
Author

commented Nov 2, 2010

The options 1 and 2 are changing the behavior for class A for the next tests, aren't they?

Option 3 works very well but this forces to me (and every one in the team) to change every access to this constant and using the wrapper method instead of the direct access to the constant. What we can say it is an intrusive test.

I have done something like this:

require 'test/unit'
require 'mocha'

class A
  MY_CONST = 'a'
end

class MyTest < Test::Unit::TestCase
  def setup
    @old_constant = A::MY_CONST
    A.const_set( :MY_CONST, 'b' )
  end

  def teardown
    A.const_set( :MY_CONST, @old_constant )
  end

  def test_should_returns_b
    assert_equal( 'b', A::MY_CONST )
  end
end

There are a couple of warnings that with the Rails silence_warnings you can hidden them.

Anyhow I still think that would be great if Mocha will offer to us the possibility of mocking a constant so we will can play with them on an easy way.

The points are, is this possible? do you think is a good idea to add this behavior to Mocha?. If both answers are yes I can try to help in this feature. If any answer is not you can close the issue :)

Regards.

@floehopper

This comment has been minimized.

Copy link
Member

commented Nov 3, 2010

You are right about options 1 & 2 - my examples were just demonstrating what was possible - they don't return the constant to its initial state, but hopefully you can see this would be trivial to implement. I understand your reservations about option 3, but I'm not sure I completely agree - I'm always happy to change my application code to make testing easier.

Anyway, just to be clear, it certainly is possible to do what you want to do. However, just because something is possible, doesn't mean it's desirable!

Personally, I don't find I need to stub constants when writing tests, so it would be good to understand more about why you want to be able to do it. Can you give us a more real-world example? It's always easier to talk about something more concrete.

If it makes any sense for the value to change for the duration of a test, I would be inclined to store it in some kind of variable instead of a constant, but perhaps use a constant for the variable's default value.

Anyway, if you'd like to discuss this further and see if other people are interested in this feature, please drop us a line on the mailing list.

Cheers, James.

@fguillen

This comment has been minimized.

Copy link
Author

commented Nov 6, 2010

Probably hasn't too much sense what I'm proposing.

Thanks @floehopper for your answers.

@mariusbutuc

This comment has been minimized.

Copy link

commented Aug 26, 2015

Still comes up high in google searches so FWIW here are my ¢2: wrap the constant in a class method

class Fetcher
  DATA_FILE = 'tmp/real_data.csv'.freeze
  #
  def self.data_file
    DATA_FILE
  end
end

that we can later stub in the test:

class FetcherTest < ActiveSupport::TestCase
  def setup
    Fetcher.stubs(:data_file).returns('test/fixtures/files/test_data.csv')
  end
  #
end
davidbasalla added a commit to alphagov/frontend that referenced this issue Feb 25, 2016
We test that custom slugs and formats get recognised and passed into the render method. We wrap the RootController Constants in methods so that we can stub them in the tests, as per freerange/mocha#13.

We wondered whether it would be better to test the RootController constants CUSTOM_SLUGS and CUSTOM_FORMATS directly with their preset keys and values (so without stubbing), but were unsure if it would make the tests harder to read.
@grosser

This comment has been minimized.

Copy link
Contributor

commented May 12, 2018

  def stub_const(const, value)
    old = A.const_get(const)
    A.send(:remove_const, const)
    A.const_set(const, value)
    yield
  ensure
    A.send(:remove_const, const)
    A.const_set(const, old)
  end
@floehopper

This comment has been minimized.

Copy link
Member

commented May 15, 2018

@grosser Do you mind me asking what prompted your comment on this issue? Are you suggesting that this functionality should be included in Mocha?

@grosser

This comment has been minimized.

Copy link
Contributor

commented May 15, 2018

not sure ... just came here and did not find a nice solution so I left the one I'm using now for future wanderers :)

@floehopper

This comment has been minimized.

Copy link
Member

commented May 15, 2018

ok - thanks!

@fwuensche

This comment has been minimized.

Copy link

commented Sep 3, 2019

Just a small improvement on top of @grosser's method:

  def stub_constant(klass, const, value)
    old = klass.const_get(const)
    klass.send(:remove_const, const)
    klass.const_set(const, value)
    yield
  ensure
    klass.send(:remove_const, const)
    klass.const_set(const, old)
  end

So you can now stub constants from different classes:

  it 'actually sends notification messages on slack' do
    stub_constant(Notification, :SLACK_CHANNEL, '#test') do
      stub_constant(User, :ADMIN_SLACK_ID, '78UIMF1') do
        Notification.master_alert
      end
    end
  end
This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
5 participants
You can’t perform that action at this time.