15

SCENARIO I have extracted a concern called Taggable. It's a module that allows any model to support tagging. I have included this concern/module into models like User, Location, Places, Projects.

I want to write tests for this module, but don't know where to start.

QUESTION
1. Can I do isolation testing on the Taggable concern?
In the example below the test fails because the test is looking for a dummy_class table. I'm assuming it's doing this because of the has_many code in Taggable so as a result it expects 'DummyClass' to be an ActiveRecord object.

# /app/models/concerns/taggable.rb
module Taggable
  extend ActiveSupport::Concern

  included do
    has_many :taggings, :as => :taggable, :dependent=> :destroy
    has_many :tags, :through => :taggings
  end

  def tag(name)
    name.strip!
    tag = Tag.find_or_create_by_name(name)
    self.taggings.find_or_create_by_tag_id(tag.id)
  end
end


# /test/models/concerns/taggable_test.rb
require 'test_helpers'

class DummyClass
end

describe Taggable do
  before do
    @dummy = DummyClass.new
    @dummy.extend(Taggable)
  end

  it "gets all tags" do
    @dummy.tag("dummy tag")
    @dummy.tags.must_be_instance_of Array
  end
end

Part of me thinks if I just test a model that has this module included inside of it like User that's enough of a test. But I keep reading that you should test modules in isolation.

Looking for some guidance / strategy on what the right approach is.

mswieboda
  • 1,026
  • 2
  • 10
  • 30
alenm
  • 1,013
  • 3
  • 15
  • 34

4 Answers4

7

I would suggest having DummyClass be a generic ActiveRecord::Base child with very little custom code besides just include Taggable, so that you would be isolating your concern module as much as possible but still being an AR class. Avoiding the use of one of your "real" classes like User still isolates you from any other code in those classes, which seems valuable.

So something like this:

class DummyClass < ActiveRecord::Base; end

describe Taggable do
  before do
    @dummy_class = DummyClass.new
  end
  ...
end

Since your DummyClass may need to actually interact with the DB to test things like associations, you may need to create temporary tables in the DB during testing. The temping Ruby gem may be able to help with that, since its designed to create temporary ActiveRecord models and their underlying database tables.

Temping allows you to create arbitrary ActiveRecord models backed by a temporary SQL table for use in tests. You may need to do something like this if you're testing a module that is meant to be mixed into ActiveReord models without relaying on a concrete class.

Stuart M
  • 11,458
  • 6
  • 45
  • 59
  • 2
    So I did try your approach before but when I run my tests it keeps thinking I have a 'dummy_classes' table. ActiveRecord::StatementInvalid: Could not find table 'dummy_classes' – alenm Apr 07 '13 at 22:01
  • 1
    You may find the [temping](https://github.com/jpignata/temping) gem useful, see my updated answer above – Stuart M Apr 07 '13 at 22:05
  • Thanks for the tip on the temping gem. But I was thinking why go through the process of using this gem when I could just test out the module in a real ActiveRecord class like "User". Maybe that is the right approach? If this module does require ActiveRecord then test with it. Great gem though. – alenm Apr 08 '13 at 00:20
  • You certainly can (and should) test the functionality that comes from Taggable within your User and other classes, but I'd argue that in order to truly isolate your Taggable testing you should _also_ test it using a "pristine" class such as your `DummyClass` that doesn't have any additional code of its own which might change how things behave. – Stuart M Apr 08 '13 at 00:24
  • 1
    I already have a test passing using my User model. So I'm good for now. But I don't have a passing test using the "DummyClass" because my test keeps looking for a dummyclasses table in the test DB because it expects DummyClass to be an ActiveRecord model. I would like to achieve this "pristine" class but it seems like the only way is by creating a dummy table in my test environment or using this gem. – alenm Apr 08 '13 at 00:52
  • 1
    Correct. And though it may be lower priority than testing the behavior via the User class, I'm just saying I'd support doing that :) – Stuart M Apr 08 '13 at 00:53
  • using the [temping](https://github.com/jpignata/temping) gem worked like a charm for me – mswieboda Jul 24 '14 at 14:53
2

I went with using ActiveRecord Tableless rather than Temping gem, which seems to be a little out of date at the moment.

I set up my test up exactly the same as Stuart M has in his answer but included the has_no_table helper method and columns required in my DummyClass.

class DummyClass < ActiveRecord::Base
  # Use ActiveRecord tableless
  has_no_table
  # Add table columns
  column :name, :string
  # Add normal ActiveRecord validations etc
  validates :name, :presence => true
end   

This worked for what I needed to test, which was a module that extended ActiveRecord::Base with a few additional methods, but I haven't tried it with any has_many associations so it still might not help with what you wanted to test.

Community
  • 1
  • 1
schinery
  • 23
  • 6
1

Here is my solution of similar problem:

describe Taggable do
  subject { mock_model('User').send(:extend, Taggable) }

  it { should have_many(:tags) }
  ...

  describe "#tag" do
    ...
  end
end

In fact mock_model('User') can mock any existent model in the system.

This is not an ideal solution but at least it's clear and mocks everything.

Note: mock_model (AR mocks) were extracted to rspec-activemodel-mocks in rspec 3.0.
Also you need to use shoulda-matchers for associations matchers.

freemanoid
  • 14,592
  • 6
  • 54
  • 77
0

As suggested in @StuartM's answer, using the temping gem worked for me:

# test.rb/spec.rb
Temping.create :dummy_class do
  include Taggable
end

describe Taggable do
  before do
    @dummy = DummyClass.new
  end
  ...
end
Community
  • 1
  • 1
mswieboda
  • 1,026
  • 2
  • 10
  • 30