How to test a hard coded class using a fake in MiniTest - ruby-on-rails

I have a PlantTree job that calls a PlantTree service object. I would like to test the job to ascertain that it instantiates the PlantTree service with a tree argument and calls the call method.
I'm not interested in what the service does or the result. It has its own tests, and I don't want to repeat those tests for the job.
# app/jobs/plant_tree_job.rb
class PlantTreeJob < ActiveJob::Base
def perform(tree)
PlantTree.new(tree).call
end
end
# app/services/plant_tree.rb
class PlantTree
def initialize(tree)
#tree = tree
end
def call
# Do stuff that plants the tree
end
end
As you can see, the PlantTree class is hard coded in the perform method of the job. So I can't fake it and pass it in as a dependency. Is there a way I can fake it for the life time of the perform method? Something like...
class PlantTreeJobTest < ActiveJob::TestCase
setup do
#tree = create(:tree)
end
test "instantiates PlantTree service with `#tree` and calls `call`" do
# Expectation 1: PlantTree will receive `new` with `#tree`
# Expectation 2: PlatTree will receive `call`
PlantTreeJob.perform_now(#tree)
# Verify that expections 1 and 2 happened.
end
end
I'm using Rails' default stack, which uses MiniTest. I know this can be done with Rspec, but I'm only interested in MiniTest. If it's not possible to do this with MiniTest only, or the default Rails stack, I'm open to using an external library.

You should be able to do something like
mock= MiniTest::Mock.new
mock.expect(:call, some_return_value)
PlantTree.stub(:new, -> (t) { assert_equal(tree,t); mock) do
PlantTreeJob.perform_now(#tree)
end
mock.verify
This stubs the new method on PlantTree, checks the argument to tree and then returns a mock instead of a PlantTree instance. That mock further verifies that call was called.

Not sure how to write this in Minitest, but you can use a mock (RSpec syntax here):
expect(PlantTree).to receive(:new).with(tree)
expect_any_instance_of(PlantTree).to receive(:call)
# NOTE: either of the above mocks (which are expectations)
# will fail if more than 1 instance receives the method you've mocked -
# that is, PlantTree#new and PlantTree#call
# In RSpec, you can also write this using `receive_message_chain`:
# expect(PlantTree).to receive_message_chain(:new, :call)
job = PlantTreeJob.new(#tree)
job.perform
This test will fail unless your PlantTree service object (1) gets instantiated via #new, and (2) gets #call'ed.
Disclaimer: this might not be 100% functional but this should be the right idea, assuming I've read OP's Q correctly.

plant_tree_mock= MiniTest::Mock.new
dummy = Object.new
tree = Object.new
plant_tree_mock.expect(:call, dummy, [tree])
PlantTree.stub(:new, plant_tree_mock) do
PlantTreeJob.perform_now(tree)
end
assert plant_tree_mock.verify

Related

In Ruby on Rails tests, how do I call from a ControllerTest a method defined inside a Controller

I have a test class called AdControllerTest, which I am using to test AdController.
From AdControllerTest, I'm trying to call a method defined in AdController, but I don't think I'm doing it right and I can't find the correct way to do this.
My test code looks like so
test "pctr to final list is correct for pctr policy" do
# Make a CTR list
# Make a selectedAds list
# Check that the CTR list reorders the selectedAds appropriately
response = AdCampaign.search query: {
bool: {
must: [
{ match: { target_gender: "F" },
match: { target_country: "KR" } } ]
}}
selectedAds = Array.new(NUMBEROFADS) {Hash.new}
for i in 1..NUMBEROFADS do
selectedAds[i-1] = response.results.to_a[i-1]
end
testCTR = [0.032521635096847835, 0.03863127908388814, 0.007986670179316374]
finalAds = AdController.pctrToAd(selectedAds: selectedAds, pctr: testCTR)
# Manually order selectedAds by testCTR and compare
comparisonAds = Array.new(NUMBEROFADS) {Hash.new}
comparisonAds[0] = selectedAds[1]
comparisonAds[1] = selectedAds[0]
comparisonAds[2] = selectedAds[2]
assert_equal(finalAds, comparisonAds)
end
And within that code I'm trying to call finalAds = AdController.pctrToAd(selectedAds: selectedAds, pctr: testCTR)
The method pctrToAd is definitely defined in AdController.
But I get an error like so:
Error:
AdControllerTest#test_pctr_to_final_list_is_correct_for_pctr_policy:
NoMethodError: undefined method `pctrToAd' for AdController:Class
test/controllers/ad_controller_test.rb:166:in `block in <class:AdControllerTest>'
Am I not supposed to call the method inside a controller that way? If not, how am I supposed to call it?
AdController.pctrToAd is calling a method on the AdController class. Presumably you want to call a method on an AdController object.
NoMethodError: undefined method `pctrToAd' for AdController:Class
^^^^^^^^^^^^^^^^^^
Assuming this is AdControllerTest, the controller object is available as #controller.
finalAds = #controller.pctrToAd(selectedAds: selectedAds, pctr: testCTR)
You don't.
The only methods inside your controller that should be public are the actions of the controller that are called by the router when it responds to HTTP requests.
Those are tested by sending HTTP requests with integration and system tests to your application.
Writing controller tests is a highly flawed and outdated approach that is not recommended by the Rails core team. Isolating controllers is actually very hard as they are Rack applications and have a hard dependency on an incoming request and the Rack middleware stack. Its also not a good idea as the extensive mocking required lets bugs slip through.
If you have a method that you want to call from the outside it does not belong in the controller. Put it in a model, service object or anywhere else where its actually easy to test it isolation. If you don't need to call/test it directly put it in a private method and test it indirectly.
Controllers have tons of responsibilities already. Don't turn them into the junk drawers of your application.
What you do instead.
Extract the logic of the method that needs to be testable in isolation. This can either be into the model layer or a Plain Old Ruby object such as a service object:
# app/services/ad_campains/frobnobizer_service.rb
module AdCampaigns
class FrobnobizerService
def initialize(selected_ads:, pctr:)
#selected_ads = selected_ads
#pctr = pctr
end
def call
# do something amazing
end
def self.call(selected_ads:, pctr:)
new(...).run
end
end
end
require "test_helper"
class AdCampaignsFrobnobizerServiceTest < Minitest::Test
test "fobnobizes the whatchamacallits" do
result = AdCampaigns::FrobnobizerService.call(
selected_ads: [1,2,3], pctr: 'foobarbaz'
)
assert_equals(result[:foo], 'bar')
end
end
When you compare this with a controller its extremely easy to test as it has very few dependencies and moving parts and only does one job.
You then test that the controller calls this component indirectly by sending a HTTP request:
class AdCampaignsController < ApplicationController
# GET /ad_campigns/frobnobize
def frobnobize
#stuff = AdCampaigns::FrobnobizerService.call(
selected_ads: [1,2,3], pctr: 'foobarbaz'
)
# do something with #stuff
end
end
require "test_helper"
class AdCampaignsFlowTest < ActionDispatch::IntegrationTest
test "GET /ad_campigns/frobnobize" do
get '/ad_campigns/frobnobize'
# test that the controller called the service by
# writing assertions/refutions about the headers
# response body or potential side effects.
end
end
In some cases you might want to use spies/mocks to ensure that the controller calls the collaborator as expected instead of testing the outcome.

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.

Ruby/Rails testing - access variable outside of scope?

I want to unit test a method with rspec for RoR and have a method like this:
def create_record(obj, params)
begin
obj.add_attributes(params)
result = obj.save
rescue
MyMailer.failed_upload(#other_var, obj.api_class_name, params).deliver_now
end
end
create_record is never invoked directly, but through another method which fills in #other_var appropriately.
How should I go about testing the code to make sure MyMailer is called correctly? Should I have passed #other_var into the method instead of relying on it being filled in elsewhere (aka: is this a code smell?)? Thanks!
In Ruby you can use Object#instance_variable_set to set any instance variable.
RSpec.describe Thing do
describe "#create_record" do
let(:thing) do
t = Thing.new
t.instance_variable_set(:#other_var, "foo")
t
end
# ...
end
end
This completely circumvents any encapsulation which means that the use of instance_variable_set can be considered a code smell.
Another alternative is to use RSpecs mocking and stubbing facilities but stubbing the actual object under test is also a code smell.
You can avoid this by passing the dependency as a parameter or by constructor injection:
class Thing
attr_accessor :other_var
def initialize(other_var: nil)
#other_var = other_var
end
def create_record(obj, attributes)
# ...
end
end
A good pattern for this is service objects.

how do I test that an instance variable is set in my my mailer with rspec?

How do I test that a certain instance variable is set in my my mailer with rspec? assigns is coming back undefined..
require File.dirname(__FILE__) + '/../../spec_helper'
describe UserMailer do
it "should send the member user password to a User" do
user = FG.create :user
user.create_reset_code
mail = UserMailer.reset_notification(user).deliver
ActionMailer::Base.deliveries.size.should == 1
user.login.should be_present
assigns[:person].should == user
assigns(:person).should == user #both assigns types fail
end
end
The error returned is:
undefined local variable or method `assigns' for #<RSpec::Core::ExampleGroup::Nested_1:0x007fe2b88e2928>
assigns is only defined for controller specs and that's done via the rspec-rails gem. There is no general mechanism to test instance variables in RSpec, but you can use Kernel's instance_variable_get to access any instance variable you want.
So in your case, if object were the object whose instance variable you were interested in checking, you could write:
expect(object.instance_variable_get(:#person)).to eql(user)
As for getting ahold of the UserMailer instance, I can't see any way to do that. Looking at the method_missing definition inside https://github.com/rails/rails/blob/master/actionmailer/lib/action_mailer/base.rb, a new mailer instance will be created whenever an undefined class method is called with the same name as an instance method. But that instance isn't saved anywhere that I can see and only the value of .message is returned. Here is the relevant code as currently defined on github:
Class methods:
def respond_to?(method, include_private = false) #:nodoc:
super || action_methods.include?(method.to_s)
end
def method_missing(method_name, *args) # :nodoc:
if respond_to?(method_name)
new(method_name, *args).message
else
super
end
end
Instance methods:
attr_internal :message
# Instantiate a new mailer object. If +method_name+ is not +nil+, the mailer
# will be initialized according to the named method. If not, the mailer will
# remain uninitialized (useful when you only need to invoke the "receive"
# method, for instance).
def initialize(method_name=nil, *args)
super()
#_mail_was_called = false
#_message = Mail.new
process(method_name, *args) if method_name
end
def process(method_name, *args) #:nodoc:
payload = {
mailer: self.class.name,
action: method_name
}
ActiveSupport::Notifications.instrument("process.action_mailer", payload) do
lookup_context.skip_default_locale!
super
#_message = NullMail.new unless #_mail_was_called
end
end
I don't think this is possible to test unless Rails changes its implementation so that it actually provides access to the ActionMailer (controller) object and not just the Mail object that is generated.
As Peter Alfvin pointed out, the problem is that it returns the 'message' here:
new(method_name, *args).message
instead of just returning the mailer (controller) like this:
new(method_name, *args)
This post on the rspec-rails list might also be helpful:
Seems reasonable, but unlikely to change. Here's why. rspec-rails
provides wrappers around test classes provided by rails. Rails
functional tests support the three questions you pose above, but rails
mailer tests are different. From
http://guides.rubyonrails.org/action_mailer_basics.html: "Testing
mailers normally involves two things: One is that the mail was queued,
and the other one that the email is correct."
To support what you'd like to see in mailer specs, rspec-rails would
have to provide it's own ExampleGroup (rather than wrap the rails
class), which would have to be tightly bound to rails' internals. I
took great pains in rspec-rails-2 to constrain coupling to public
APIs, and this has had a big payoff: we've only had one case where a
rails 3.x release required a release of rspec-rails (i.e. there was a
breaking change). With rails-2, pretty much every release broke
rspec-rails because rspec-rails was tied to internals (rspec-rails'
fault, not rails).
If you really want to see this change, you'll need to get it changed
in rails itself, at which point rspec-rails will happily wrap the new
and improved MailerTestCase.

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