14

I'm having a bad time finding on SO/Google this particular case. I have a module with functions and in order to use them, you have to create a Class which includes/extends the Module depending if you'll want instance methods or class methods.

module A
  def say_hello name
    "hello #{name}"
  end

  def say_bye
    "bye"
  end
end

How can I test this module using rspec?

I have something like this, and I'm not sure where is the point I should create the class and extend Module.

describe A do
  class MyClass
    extend A
  end

  before(:each) { @name = "Radu" }

  describe "#say_hello" do
    it "should greet a name" do
      expect(Myclass.say_hello(@name)).to eq "hello Radu"
    end
  end
end

Thank you!

Henrik N
  • 15,786
  • 5
  • 82
  • 131
radubogdan
  • 2,744
  • 1
  • 19
  • 27

2 Answers2

29

You can create an anonymous class in your tests:

describe A do
  let(:extended_class) { Class.new { extend A } }
  let(:including_class) { Class.new { include A } }

  it "works" do
    # do stuff with extended_class.say_hello
    # do stuff with including_class.new.say_hello
  end
end

To see something similar in real code, I've used this strategy for testing my attr_extras lib.

That said, include and extend are standard features of Ruby, so I wouldn't test that every module works both when including and when extending – that's usually a given.

If you create a named class in the test, like you do in your question, I believe that class will exist globally for the duration of your test run. So this class will leak between every test of your test suite, potentially causing conflicts somewhere.

If you use let to create an anonymous class, it will only be available inside this particular test. There is no global constant pointing to it that could conflict with other tests.

You could also use RSpec's stub_const to get a constant that doesn't leak, if you need to:

stub_const("MyClass", Class.new { … })
# do stuff with MyClass

Note that you'd run stub_const inside a before or it. Not just at the top of the file or in the class context.

Henrik N
  • 15,786
  • 5
  • 82
  • 131
  • I still get `undefined method `my_method' for #`, but perhaps is my fault. Thank you for your quick answer. – radubogdan Dec 07 '14 at 11:26
  • @radubogdan I tried pasting [this exact code (Gist link)](https://gist.github.com/henrik/8d400883a99779de4926) to an existing RSpec test and it passes, so maybe something else is wrong? – Henrik N Dec 07 '14 at 11:29
  • Yes, for sure something else is wrong, but I can't find what. Could be spec_helper? (I created spec_helper using rspec --init, rspec version is 3.1.7) – radubogdan Dec 07 '14 at 11:49
  • @radubogdan No idea, I'm afraid. Try removing/tweaking things and see if you can pinpoint the issue, and ask a separate question about it, perhaps? – Henrik N Dec 07 '14 at 11:59
  • If I replace `require 'spec_helper'` with `require_relative '../lib/a.rb'` is working good. – radubogdan Dec 07 '14 at 12:27
  • @radubogdan Aha. Yes, you need to require the tested class or module in your test file. If you're using Rails it may automatically do it for you in most cases, but that's not true for Ruby in general. – Henrik N Dec 07 '14 at 12:31
2

To help out future readers, here's an example I got going using @henrik-n 's solution:

# slim_helpers.rb
module SlimHelpers

  # resourceToTitle converts strings like 'AWS::AutoScaling::AutoScalingGroup'
  # to 'Auto Scaling Group'
  def resourceToTitle(input)
    input.split('::')[-1].gsub(/([A-Z])/, ' \1').lstrip
  end

end

# slim_helpers_spec.rb
require_relative '../slim_helpers'

describe SlimHelpers do

  # extended class
  let(:ec) { Class.new { extend SlimHelpers } }

  it "converts AWS resource strings to titles" do
    out = ec.resourceToTitle('AWS::AutoScaling::AutoScalingGroup')
    expect(out).to eq 'Auto Scaling Group'
  end
end
Sonia Hamilton
  • 4,229
  • 5
  • 35
  • 50