Rails: How do I write tests for a ruby module?

Ruby on-RailsRubyUnit TestingTestingModule

Ruby on-Rails Problem Overview


I would like to know how to write unit tests for a module that is mixed into a couple of classes but don't quite know how to go about it:

  1. Do I test the instance methods by writing tests in one of the test files for a class that includes them (doesn't seem right) or can you somehow keep the tests for the included methods in a separate file specific to the module?

  2. The same question applies to the class methods.

  3. Should I have a separate test file for each of the classes in the module like normal rails models do, or do they live in the general module test file, if that exists?

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

IMHO, you should be doing functional test coverage that will cover all uses of the module, and then test it in isolation in a unit test:

setup do
  @object = Object.new
  @object.extend(Greeter)
end

should "greet person" do
  @object.stubs(:format).returns("Hello {{NAME}}")
  assert_equal "Hello World", @object.greet("World")
end

should "greet person in pirate" do
  @object.stubs(:format).returns("Avast {{NAME}} lad!")
  assert_equal "Avast Jim lad!", @object.greet("Jim")
end

If your unit tests are good, you should be able to just smoke test the functionality in the modules it is mixed into.

Or…

Write a test helper, that asserts the correct behaviour, then use that against each class it's mixed in. Usage would be as follows:

setup do
  @object = FooClass.new
end

should_act_as_greeter

If your unit tests are good, this can be a simple smoke test of the expected behavior, checking the right delegates are called etc.

Solution 2 - Ruby on-Rails

Use inline classes (I am not doing any fancy flexmock or stubba/mocha usage just to show the point)

def test_should_callout_to_foo
   m = Class.new do
     include ModuleUnderTest
     def foo
        3
     end
   end.new
   assert_equal 6, m.foo_multiplied_by_two
 end

Any mocking/stubbing library out there should give you a cleaner way to do this. Also you can use structs:

 instance = Struct.new(:foo).new
 class<<instance
     include ModuleUnderTest
 end
 instance.foo = 4

If I have a module that is being used in many places I have a unit test for it which does just that (slide a test object under the module methods and test if the module methods function properly on that object).

Solution 3 - Ruby on-Rails

What I like to do is create a new host class and mix the module into it, something like this:

describe MyModule do
  let(:host_class) { Class.new { include MyModule } }
  let(:instance) { host_class.new }

  describe '#instance_method' do
    it 'does something' do
      expect(instance.instance_method).to do_something
    end
  end
end

Solution 4 - Ruby on-Rails

I try to keep my tests focused only on the contract for that particular class/module. If I've proven the module's behavior in a test class for that module (usually by including that module in a test class declared in the spec for that module) then I won't duplicate that test for a production class that uses that module. But if there's additional behavior that I want to test for the production class, or integration concerns, I'll write tests for the production class.

For instance I have a module called AttributeValidator that performs lightweight validations kind of similar to ActiveRecord. I write tests for the module's behavior in the module spec:

before(:each) do
  @attribute_validator = TestAttributeValidator.new
end

describe "after set callbacks" do
  it "should be invoked when an attribute is set" do
    def @attribute_validator.after_set_attribute_one; end
    @attribute_validator.should_receive(:after_set_attribute_one).once
    @attribute_validator.attribute_one = "asdf"
  end
end

class TestAttributeValidator 
    include AttributeValidator
    validating_str_accessor [:attribute_one, /\d{2,5}/]      
end

Now in a production class that includes the module, I won't re-assert that the callbacks are made, but I may assert that the included class has a certain validation set with a certain regular expression, something particular to that class, but not reproducing the tests I wrote for the module. In the spec for the production class, I want to guarantee that particular validations are set, but not that validations work in general. This is a kind of integration test, but one that doesn't repeat the same assertions I made for the module:

describe "ProductionClass validation" do
  it "should return true if the attribute is valid" do
    @production_class.attribute = @valid_attribute 
    @production_class.is_valid?.should be_true
  end
  it "should return false if the attribute is invalid" do
    @production_class.attribute = @invalid_attribute
    @production_class.is valid?.should be_false
  end
end

There is some duplication here (as most integration tests will have), but the tests prove two different things to me. One set of tests prove the general behavior of the module, the other proves particular implementation concerns of a production class that uses that module. From these tests I know that the module will validate attributes and perform callbacks, and I know that my production class has a specific set of validations for specific criteria unique to the production class.

Hope that helps.

Solution 5 - Ruby on-Rails

In minitest since each test is explicitly a class you can just include the module to the test and test the methods:

class MyModuleTest < Minitest::Test
   include MyModule

   def my_module_method_test
     # Assert my method works
   end
end

Solution 6 - Ruby on-Rails

I would generally test the module in as much isolation as possible, essentially testing the methods, with just enough code, mocks and stubs to get it working.

I would then probably also have tests for the classes the modules is included in. I may not test every class, but would test enough of the classes to get good coverage and have insight into any issues that arise. These tests don't need to explicitly test the module, but would certainly test it's usage in particular scenarios.

Each set of tests would have its own file.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestiontsdbrownView Question on Stackoverflow
Solution 1 - Ruby on-RailscwninjaView Answer on Stackoverflow
Solution 2 - Ruby on-RailsJulikView Answer on Stackoverflow
Solution 3 - Ruby on-RailsMarnen Laibow-KoserView Answer on Stackoverflow
Solution 4 - Ruby on-RailsDave SimsView Answer on Stackoverflow
Solution 5 - Ruby on-RailslcguidaView Answer on Stackoverflow
Solution 6 - Ruby on-RailsToby HedeView Answer on Stackoverflow