how to write and inherit from an abstract subclass of ActionController::TestCase - ruby-on-rails

I have a bunch of Rails 3.1 controllers which all have very similar testing requirements. I have extracted out the common code (all Test::Unit style), e.g. the following three tests are completely reusable across all of them:
def create
new_record = { field_to_update => new_value }
create_params = { :commit => "Create", :record => new_record }
post :create, create_params
end
test "should_not_create_without_login" do
assert_no_difference(count_code) do create; end
assert_need_to_log_in
end
test "should_not_create_without_admin_login" do
login_as_non_admin
assert_no_difference(count_code) do create; end
assert_needs_admin_login
end
test "should_create" do
login_as_admin
assert_difference(count_code) do create; end
assert_redirected_to list_path
end
and I intended that it could go in an abstract class which inherits from ActionController::TestCase. Then each functional test would only need to override the abstract methods, ending up pleasingly small and clean, e.g.
class Admin::AvailabilitiesControllerTest < Admin::StandardControllerTest
tests Admin::AvailabilitiesController
def model ; Availability end
def id_to_change ; availabilities(:maybe).id end
def field_to_update; :value end
def new_value ; 'maybe2' end
def list_path ; admin_availabilities_path end
end
However, when I try this, it appears that the framework tries to run the test methods directly from the abstract class, rather than from the inherited class:
E
===================================================================================================
Error:
test_should_not_create_without_login(Admin::ControllerTestBase):
NoMethodError: undefined method `model' for test_should_not_create_without_login(Admin::ControllerTestBase):Admin::ControllerTestBase
test/lib/admin_controller_test_base.rb:7:in `count_code'
test/lib/admin_controller_test_base.rb:68:in `block in <class:ControllerTestBase>'
===================================================================================================
I've heard that other testing frameworks and gems can provide mechanisms for meta-programming of tests, so maybe I'm going about this in entirely the wrong way. But I've tried several things and looked at RSpec, coulda, shoulda, context, contest ... and I still can't see a way to achieve what I'm after. Any ideas? Thanks!

I finally figured this out - once I realised that this is a general Ruby Test::Unit issue rather a Rails testing issue, a quick google instantly revealed How do I inherit abstract unit tests in Ruby? which already had a good answer. Then the only missing piece was being able to use the syntactic sugar:
test "something should behave in a certain way" do
...
end
rather than
def test_something_should_behave_in_a_certain_way" do
...
end
I found the answer to this within the ActiveSupport codebase itself, under lib/active_support/test_case.rb:
extend ActiveSupport::Testing::Declarative
This module defines test as a class method (which is why extend is required rather than include).
So the complete solution looks like this:
# tests/functional/admin/availabilities_controller_test.rb
class Admin::AvailabilitiesControllerTest < ActionController::TestCase
tests Admin::AvailabilitiesController
include Admin::ControllerTests
# non-reusable tests and helper methods specific to this
# controller test go here
end
# lib/admin/controller_tests.rb
module Admin::ControllerTests
extend ActiveSupport::Testing::Declarative
test "this test can be reused by anything which includes this module" do
...
end
end
The downside of this module-based approach is that you can't override the included tests. I guess that's just a fundamental limitation of Test::Unit - maybe the best answer is to move to RSpec, but I don't know it well enough yet to be sure.

Related

Unit testing code that references Rails models without loading the models

I am trying to unit test a Plain Old Ruby Object that has a method which calls a class method on a Rails model. The Rails app is quite large (10s of seconds to load) so I'd prefer to avoid loading all of Rails to do my unit test which should run in under 1s.
Example:
class Foo
def bar
SomeRailsModel.quxo(3)
end
end
RSpec.describe Foo do
let(:instance) { Foo.new }
it 'calls quxo on SomeRailsModel' do
expect(SomeRailsModel).to receive(:quxo)
instance.bar
end
end
The problem here is that I need to require 'rails_helper' to load up Rails in order for app/models/some_rails_model to be available. This leads to slow unit tests due to Rails dependency.
I've tried defining the constant locally and then using regular spec_helper which kind of works.
Example:
RSpec.describe Foo do
let(:instance) { Foo.new }
SomeRailsModel = Object.new unless Kernel.const_defined?(:SomeRailsModel)
it 'calls quxo on SomeRailsModel' do
expect(SomeRailsModel).to receive(:quxo)
instance.bar
end
end
This code lets me avoid loading all of Rails and executes very fast. Unfortunately, by default (and I like this) RSpec treats the constant as a partial double and complains that my SomeRailsModel constant doesn't respond to the quxo message. Verifying doubles are nice and I'd like to keep that safety harness. I can individually disable the verification by wrapping it in a special block defined by RSpec.
Finally, the question. What is the recommended way to have fast unit tests on POROs that use Rails models without requiring all of Rails while also keeping verifying doubles functionality enabled? Is there a way to create a "slim" rails_helper that can just load app/models and the minimal subset of ActiveRecord to make the verification work?
After noodling a few ideas with colleagues, here is the concensus solution:
class Foo
def bar
SomeRailsModel.quxo(3)
end
end
require 'spec_helper' # all we need!
RSpec.describe Foo do
let(:instance) { Foo.new }
let(:stubbed_model) do
unless Kernel.const_defined?("::SomeRailsModel")
Class.new { def self.quxo(*); end }
else
SomeRailsModel
end
end
before { stub_const("SomeRailsModel", stubbed_model) }
it 'calls quxo on SomeRailsModel' do
expect(stubbed_model).to receive(:quxo)
instance.bar
end
end
When run locally, we'll check to see if the model class has already been defined. If it has, use it since we've already paid the price to load that file. If it isn't, then create an anonymous class that implements the interface under test. Use stub_const to stub in either the anonymous class or the real deal.
For local tests, this will be very fast. For tests run on a CI server, we'll detect that the model was already loaded and preferentially use it. We get automatic double method verification too in all cases.
If the real Rails model interface changes but the anonymous class falls behind, a CI run will catch it (or an integration test will catch it).
UPDATE:
We will probably DRY this up a bit with a helper method in spec_helper.rb. Such as:
def model_const_stub(name, &blk)
klass = unless Kernel.const_defined?('::' + name.to_s)
Class.new(&blk)
else
Kernel.const_get(name.to_s)
end
stub_const(name.to_s, klass)
klass
end
# DRYer!
let(:model) do
model_const_stub('SomeRailsModel') do
def self.quxo(*); end
end
end
Probably not the final version but this gives a flavor of our direction.

Testing class which uses refinements with RSpec

Let's say I've got refinement
module RefinedString
refine String do
def remove_latin_letters
#code code code code
end
end
end
and I use it inside my class Speech:
class Speech
using RefinedString
def initialize(text)
#content = text.remove_latin_letters
end
end
I've written tests for refinement in RSpec and now I'm testing Speech class
describe Speech
let(:text) { "ąńńóyińg" }
it 'should call my refinement' do
expect(text).to receive(:remove_latin_letters)
Speech.new(text)
end
end
but I get RSpec::Mocks::MockExpectationError: "ąńńóyińg" does not implement: remove_latin_letter
I don't think mocking it is a good solution (but I may be wrong! Is mocking the solution here?)
so I tried
let(:text) { described_class::String.new("ąńńóyińg") }
but the result is the same.
I don't want to explicitly call using RefinedString inside my RSpec (it should figure it out on its own, right?)
How to make RSpec aware of my refined methods?
We always want to test behavior, rather than implementation. To my mind, refinements change the behavior of other classes by virtue of being included, rather than having their own behavior. To use a somewhat clumsy analogy, if we were to test the reproductive behavior of a virus, we would have to introduce it into a host cell. We are interested in what happens to the host when the virus takes over (so to speak).
One approach is to build test classes with and without the refinement, e.g.:
class TestClass
attr_reader :content
def initialize(text)
#content = text.remove_latin_letters
end
end
describe "when not using RefinedString" do
it "raises an exception" do
expect { TestClass.new("ąńńóyińg") }.to raise_error(NoMethodError)
end
end
class RefinedTestClass
using RefinedString
attr_reader :content
def initialize(text)
#content = text.remove_latin_letters
end
end
describe "when using RefinedString" do
it "removes latin letters" do
expect(RefinedTestClass.new("ąńńóyińg").content).to eq "ńńóń"
end
end

Organizing API-Calls in callbacks

We are maintaining several Rails-Apps which all pose a similar problem that we don't have a really good solution to: All these apps contain models that need to make a API-Call to an external service in their lifecycle.
Possible cases:
User is subscribed to a Newsletter-subscriber-list, when successfully created
Prices for an offer are synced with an external shopping-system after updating
Product is updated in the Search-Index after updating
What we exprienced to NOT be a good solution: Adding these calls to the after_*callbacks of the model. Since that breaks tests fast, cause all factories now have to deal with the api-calls.
I'm looking for a good way to organize these API-call. How do you guys do this?
Ideas we came up with, which I considered not real ideal:
Moving those callbacks to the controller. Now they get easily forgotten, when creating an object
Spawning an asynchronous worker to handle the api-call. Then every - even small app - needs to have the overhead of a delayed job-queue, like sidekiq.
If you are concerned about testing you could put the callback methods into a separate class and mock the callback class during testing. Here's an example using RSpec, given the following Foo and FooCallbacks classes:
class Foo < ActiveRecord::Base
after_save FooCallbacks
end
class FooCallbacks
def self.after_save
fail "Call to external API"
end
end
You can write and successfully run a spec like this:
describe Foo do
before do
allow(FooCallbacks).to receive(:after_save)
end
it "should not invoke real APIs" do
Foo.create
end
end
This is how I now did it, after the advise:
In Foo:
class Foo < ActiveRecord::Base
before_save Foo::DataSync
end
Foo:DataSynclooks like this:
class Foo::DataSync
def self.before_save(foo)
...do the API-Calls...
end
end
Now for testing in rspec I added this:
To spec_helper.rb:
config.before(:each) do
Foo::DataSync.stub(:before_save)
end
Note that config.before(:suite) will not work, since Foo:DataSync is not loaded at that time.
Now foo_spec.rb contains just this:
describe Foo do
let(:foo) {create(:foo)}
it "will sync its data before every save" do
expect(Foo::DataSync).to receive(:before_save).with(foo)
foo.save
end
end
The Foo::DataSync can be tested like this:
describe Foo::DataSync do
let!(:foo) {create(:foo)}
before do
Foo::DataSync.unstub(:before_save)
end
after do
Foo::DataSync.stub(:before_save)
end
describe "#before_save" do
...my examples...
end
end

How do I set up a custom RSpec 1 example group?

I'm using RSpec 1 and rspec-rails to test my Rails 2.3.14 application. In this same application, I'm using Draper to provide decorators for my models. I want to be able to test my decorators.
I've got specs in spec/decorators, but as best as I can tell, because rspec-rails doesn't recognize the decorators/ path, and therefore doesn't wire up any of the extra Rails stuff into the specs.
How do I set up RSpec to recognize my spec/decorators path, and cause it to include the functionality I need (which is going to be route/helper functionality)?
I notice that RSpec has things like HelperExampleGroup, ControllerExampleGroup, etc, and I suspect that these implicitly map to spec/helpers and spec/controllers and such, but I'm unclear as to how to leverage this to set up my own DecoratorHelperGroup.
I feel like I'm 90% of the way there, but can't quite make that final connection. Examples would be most valuable, but I'll take an abstract, as well.
Solved it. The magic sauce is Spec::Example::ExampleGroupFactory.register
For the record, here's my complete spec/support/decorators.rb
module Spec
module Rails
module Example
class DecoratorExampleGroupController < ApplicationController
attr_reader :template
def view_context
template
end
end
# spec/decorators
class DecoratorExampleGroup < FunctionalExampleGroup
if ActionView::Base.respond_to?(:load_helpers) # Rails 2.0.x
ActionView::Helpers.constants.each do |name|
const = ActionView::Helpers.const_get(name)
include const if name.include?("Helper") && Module === const
end
elsif ActionView::Base.respond_to?(:helper_modules) # Rails 2.1.x
ActionView::Base.helper_modules.each do |helper_module|
include helper_module
end
else # Rails 2.2.x
include ActionView::Helpers
end
tests DecoratorExampleGroupController
class << self
def decorate(options = {})
self.subject { described_class.new(yield, options) }
end
end
before :each do
#controller.template.request = #request
#controller.set_current_view_context
end
Spec::Example::ExampleGroupFactory.register(:decorator, self)
protected
def _assigns_hash_proxy
#_assigns_hash_proxy ||= AssignsHashProxy.new(self) {#response.template}
end
end
end
end
end
All this effectively does is register specs under spec/decorators to operate as view specs (which gets me all the pieces I need). Before each decorator spec, calling #set_current_view_context on the controller invokes the Draper bits necessary to wire helpers up into my decorators. I also added a decorate method to use the current described decorator class to decorate an arbitrary object, allowing easy decoration of objects for testing. Mission accomplished!

Mocking/stubbing a method that's included from "instance.extend(DecoratorModule)"

I use a decorator module that get's included in a model instance (through the "extends" method). So for example :
module Decorator
def foo
end
end
class Model < ActiveRecord::Base
end
class ModelsController < ApplicationController
def bar
#model = Model.find(params[:id])
#model.extend(Decorator)
#model.foo
end
end
Then I would like in the tests to do the following (using Mocha) :
test "bar" do
Model.any_instance.expects(:foo).returns("bar")
get :bar
end
Is this possible somehow, or do you have in mind any other way to get this functionality???
Just an Assumption Note: I will assume that your Decorator foo method returns "bar" which is not shown in the code that you sent. If I do not assume this, then expectations will fail anyway because the method returns nil and not "bar".
Assuming as above, I have tried the whole story as you have it with a bare brand new rails application and I have realized that this cannot be done. This is because the method 'foo' is not attached to class Model when the expects method is called in your test.
I came to this conclusion trying to follow the stack of called methods while in expects. expects calls stubs in Mocha::Central, which calls stubs in Mocha::ClassMethod, which calls *hide_original_method* in Mocha::AnyInstanceMethod. There, *hide_original_method* does not find any method to hide and does nothing. Then Model.foo method is not aliased to the stubbed mocha method, that should be called to implement your mocha expectation, but the actual Model.foo method is called, the one that you dynamically attach to your Model instance inside your controller.
My answer is that it is not possible to do it.
It works (confirmed in a test application with render :text)
I usually include decorators (instead of extending them at runtime) and I avoid any_instance since it's considered bad practice (I mock find instead).
module Decorators
module Test
def foo
"foo"
end
end
end
class MoufesController < ApplicationController
def bar
#moufa = Moufa.first
#moufa.extend(Decorators::Test)
render :text => #moufa.foo
end
end
require 'test_helper'
class MoufesControllerTest < ActionController::TestCase
# Replace this with your real tests.
test "bar" do
m = Moufa.first
Moufa.expects(:find).returns(m)
m.expects(:foo).returns("foobar")
get :bar, {:id => 32}
assert_equal #response.body, "foobar"
end
end
Ok, now I understand. You want to stub out a call to an external service. Interesting that mocha doesn't work with extend this way. Besides what is mentioned above, it seems to be because the stubbed methods are defined on the singleton class, not the module, so don't get mixed in.
Why not something like this?
test "bar" do
Decorator = Module.new{ def foo; 'foo'; end }
get :bar
end
If you'd rather not get the warnings about Decorator already being defined -- which is a hint that there's some coupling going on anyway -- you can inject it:
class ModelsController < ApplicationController
class << self
attr_writer :decorator_class
def decorator_class; #decorator_class ||= Decorator; end
end
def bar
#model = Model.find(params[:id])
#model.extend(self.class.decorator_class)
#model.foo
end
end
which makes the test like:
test "bar" do
dummy = Module.new{ def foo; 'foo'; end }
ModelsController.decorator_class = dummy
get :bar
end
Of course, if you have a more complex situation, with multiple decorators, or decorators defining multiple methods, this may not work for you.
But I think it is better than stubbing the find. You generally don't want to stub your models in an integration test.
One minor change if you want to test the return value of :bar -
test "bar" do
Model.any_instance.expects(:foo).returns("bar")
assert_equal "bar", get(:bar)
end
But if you are just testing that a model instance has the decorator method(s), do you really need to test for that? It seems like you are testing Object#extend in that case.
If you want to test the behavior of #model.foo, you don't need to do that in an integration test - that's the advantage of the decorator, you can then test it in isolation like
x = Object.new.extend(Decorator)
#.... assert something about x.foo ...
Mocking in integration tests is usually a code smell, in my experience.

Resources