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

22,704

Solution 1

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

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

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

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 5

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.

Share:
22,704
tsdbrown
Author by

tsdbrown

Updated on September 23, 2020

Comments

  • tsdbrown
    tsdbrown over 3 years

    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?

  • tsdbrown
    tsdbrown over 14 years
    Thanks, I agree with what you your saying about testing the functionality in the classes it's included in. So would you have a test file for each additional class in the module, or a test file for the module as a whole? I guess I'm more hung up on the actual test files (filenames, locations etc) as opposed to what to test.
  • tsdbrown
    tsdbrown over 14 years
    When you say "functional test coverage" I guess your referring to the functionality that the models acquire and not controller tests stored test/functional? Thanks for your answer I like the idea of testing the module in isolation and writing a helper the other classes can call that use that module.
  • cwninja
    cwninja over 14 years
    By functional I mean from the outside in. This is usually a controller test, but not always. Either way, functional coverage should touch (or at least graze) all areas of the system. If your unit tests are strong, then functional testing is often enough to cover your ass. <rant> Writing too many low level tests can be a bad investment. If it is never going to fail alone, then does it catch bugs? Is the "probable debug time saved" * "probability of a bug" > "time to write the test"? Ignore this if a bug could kill people or your business. </rant>
  • Marnen Laibow-Koser
    Marnen Laibow-Koser over 12 years
    No. Controller tests are (almost) always bad ideas (Cucumber stories do the same thing better), and they're not relevant to the issue at hand anyway. Just unit-test as in the first code sample.
  • Prakash Murthy
    Prakash Murthy about 11 years
    Thanks for a comprehensive answer with examples.
  • mrm
    mrm almost 10 years
    The downside to this approach is that it actually creates a class that can collide with other tests. See the higher-rated answers for approaches that don't leave side-effects.
  • Chris
    Chris almost 6 years
    That looks about as complex as i can handle right now :)
  • Marnen Laibow-Koser
    Marnen Laibow-Koser over 5 years
    I don’t recommend this since it pollutes the namespace of the test itself. See my answer for a way to keep it separate.
  • Brian
    Brian over 5 years
    I'm fairly new to Ruby testing like this, so please correct my ignorance here. It looks like your test is tautological - you are stubbing out the method, and checking the response...to the method you stubbed. If the underlying code changes, your test will continue to pass so long as the method "greet" remains and calls something called "format", regardless of what the real methods do. Is this a correct assessment?
  • Anton Semenichenko
    Anton Semenichenko almost 5 years
    { Class.new { include MyModule } } +1