How to use mocks correctly? - ruby-on-rails

I have this class:
class EnablePost
def initialize(post_klass, id)
raise "oops" if post_klass.blank?
#post_klass = post_klass
#id = id
end
def perform
post = #post_klass.find_by_id(#id)
return unless post
post.update_attribute :enabled, true
end
end
The spec I have to write to test the above:
describe EnablePost do
it "should enable a post" do
post = mock
post.should_receive(:blank?).and_return(false)
post.should_receive(:find_by_id).with(22).and_return(post)
post.should_receive(:update_attribute).with(:enabled, true)
result = EnablePost.new(Post, 22).perform
result.should be_true
end
end
But what I really want to do is treat EnablePost as a black box. I don't want to have to mock :blank?, :find_by_id or :update_attribute.
That is to say I want my spec to look like:
describe EnablePost do
it "should enable a post" do
post = mock
result = EnablePost.new(post, 22).perform
result.should be_true
end
end
What am I missing here? Am I using mocks incorrectly?

Yes, you're confusing mocks and stubs.
A good mock explanation: http://jamesmead.org/talks/2007-07-09-introduction-to-mock-objects-in-ruby-at-lrug/
Mocks:
Different things to different people
Ambiguous terminology
Confusion with Rails “mocks”
Mock Object:
Expected method invocations set in advance
Verifies actual invocations match expected ones
Also check out http://martinfowler.com/articles/mocksArentStubs.html [thanks to user Zombies in the comments]
If you're using RSpec, it aliases double, mock, and stub. RSpec expects you to choose whichever method name makes your code clearest.
Your first chunk of test code is using the word "mock" correctly. You're setting up the method invocations that you expect to be called, in advance, and then performing them.
However, you're testing two different areas of your code: the first area is the initialize method, the second is the #perform method.
You may find it easier to mock and stub if you write smaller methods:
# What you want to test here is the raise and the member variables.
# You will stub the post_klass.
def initialize(post_klass, post_id) # post_id is a better name
raise "oops" if post_klass.blank?
#post_klass = post_klass
#post_id = post_id # because we don't want to mask Object#id
end
attr_accessor :post_id
attr_accessor :post_klass
# What you want to test here is the post_klass calls #find_by_id with post_id.
# See we've changed from using instance variables to methods.
def post
post_klass.find_by_id(post_id)
end
# What you want to test here is if the update happens.
# To test this, stub the #post method.
def perform
p = post
return unless p
p.update_attribute :enabled, true
end
When you write your code this way, you make it easy to stub the #post method.
See this for RSpec example source code showing the difference between mock and stub:
http://blog.firsthand.ca/2011/12/example-using-rspec-double-mock-and.html

Related

How to mock a chain of methods in Minitest

It looks like the only source of information for stubbing a chain of methods properly are 10+ years ago:
https://www.viget.com/articles/stubbing-method-chains-with-mocha/
RoR: Chained Stub using Mocha
I feel pretty frustrated that I can't find information of how to do this properly. I want to basically mock Rails.logger.error.
UPDATE: I basically want to do something as simple as
def my_action
Rails.logger.error "My Error"
render json: { success: true }
end
And want to write a test like this:
it 'should call Rails.logger.error' do
post my_action_url
???
end
I think maybe you misunderstood the term chain of methods in this case, they imply the chain of ActiveRecord::Relation those be able to append another. In your case Rails.logger is a ActiveSupport::Logger and that's it. You can mock the logger and test your case like this:
mock = Minitest::Mock.new
mock.expect :error, nil, ["My Error"] # expect method error be called
Rails.stub :logger, mock do
post my_action_url
end
mock.verify
Beside that, I personally don't like the way they test by stub chain of method, it's so detail, for example: i have a test case for query top ten users and i write a stub for chain of methods such as User.where().order()..., it's ok at first, then suppose i need to refactor the query or create a database view top users for optimize performance purpose, as you see, i need to stub the chain of methods again. Why do we just treat the test case as black box, in my case, the test case should not know (and don't care) how i implement the query, it only check that the result should be the top ten users.
update
Since each request Rails internal call Logger.info then if you want ignore it, you could stub that function:
def mock.info; end
In case you want to test the number of method called or validate the error messages, you can use a spy (i think you already know those unit test concepts)
mock = Minitest::Mock.new
def mock.info; end
spy = Hash.new { |hash, key| hash[key] = [] }
mock.expect(:error, nil) do |error|
spy[:error] << error
end
Rails.stub :logger, mock do
post my_action_url
end
assert spy[:error].size == 1 # call time
assert spy[:error] == ["My Error"] # error messages
It's better to create a test helper method to reuse above code. You can improve that helper method behave like the mockImplementation in Jest if you want.

RSpec Tests For Method Return & Inheritance

I am trying to write two RSpec tests for two different problems that are much more advanced that what I'm used to writing.
What I'm trying to test within my controller:
def index
#buildings ||= building_class.active.where(place: current_place)
end
My attempt at writing the RSpec test:
describe 'GET :index' do
it "assigns #buildings" do
#buildings ||= building_class.active.where(place: current_place)
get :index
expect(assigns(:buildings)).to eq([building])
end
end
This test failed and wouldn't even run so I know I'm missing something.
My second test is needing to test the returned value of a class method. Here is what I am needing to test within the controller:
def class_name
ABC::Accountant::Business
end
Here is my attempt at testing this method:
describe "class name returns ABC::Accountant::Business" do
subject do
expect(subject.class_name).to eq(ABC::Accountant::Business)
end
end
For the first test I would do something like this:
First, I would move that .active.where(place: current_place) to a scope (I'm guessing building_class returns Building or something like that):
class Building << ApplicationRecord
scope :active_in, -> (place) { active.where(place: place)
Then it's easier to stub for the test
describe 'GET :index' do
it "assigns #buildings" do
scoped_buildings = double(:buildings)
expect(Building).to receive(:active_in).and_return(scoped_buildings)
get :index
expect(assigns(:buildings)).to eq(scoped_buildings)
end
end
Then your controller will do
#buildings ||= building_class.active_in(current_place)
This way you are testing two things: that the controller actually calls the scope and that the controller assigns the returned value on the #buildings variable (you don't really need to test the actual buidlings, you can test the scope on the model spec).
Personally, I feel like it would be better to do something like #buildings = current_place.active_buildings using the same idea of the scope to test that you are getting the active buildings of the current place.
EDIT: if you can't modify your controller, then the stubbing is a little different and it implies some chaining of methods that I don't like to explicitly test.
scoped_buildings = double(:buildings)
controller.stub_chain(:building_class, :active, :where).and_return(scoped_building)
get :index
expect(assings(:buildings)).to eq scoped_buildings
Note that now your test depends on a specific implementation and testing implementation is a bad practice, one should test behaviour and not implementation.
For the second, I guess something like this should work:
describe ".class_name" do
it "returns ABC::Accountant::Business" do
expect(controller.class_name).to eq(ABC::Accountant::Business)
end
end
IMHO, that the method's name if confusing, class_name gives the idea that it returns a string, you are not returnin a name, you are returning a class. Maybe you can change that method to resource_class or something less confusing.

False positive with Rspec's "expect to receive"

I've realized that the way I've been writing tests is producing false positives.
Say I have this source code
class MyClass
def foo
end
def bar
1
end
end
The foo method does nothing, but say I want to write a test that makes sure it calls bar under the hood (even though it doesn't). Furthermore, I want to ensure that the result of calling bar directly is 1.
it "test" do
inst = MyClass.new
expect(inst).to receive(:bar).and_call_original
inst.foo
expect(inst.bar).to eq(1)
end
So this is returning true, but I want it to return false.
I want this line:
expect(inst).to receive(:bar).and_call_original
to not take into account the fact that in my test case I'm calling inst.bar directly. I want it to only look at the internal of the foo method.
You'r defining 2 separate test cases within one test case. You should change it to 2 separate tests.
describe '#bar' do
it "uses #foo" do
inst = MyClass.new
allow(inst).to receive(:foo).and_call_original
inst.bar
expect(inst).to have_received(:foo)
end
it "returns 1" do
inst = MyClass.new
# if you don't need to mock it, don't do it
# allow(inst).to receive(:foo).and_call_original
expect(inst.bar).to eq(1)
end
# if you really, really wan't to do it your way, you can specify the amount of calls
it "test" do
inst = MyClass.new
allow(inst).to receive(:foo).and_call_original
inst.foo
expect(inst.bar).to eq(1)
expect(inst).to have_received(:foo).twice # or replace .twice with .at_least(2).times
end
end
Stubs are typically used in two ways:
Check if the method was called i.e. expect_any_instance_of(MyClass).to receive(:foo) in this case what it returns is not really imortant
To simulate behaviour allow_any_instance_of(MyClass).to receive(:method).and_return(fake_response). This is a great way to avoid database interactions and or isolate out other dependencies in tests.
For example in a test that requires data setup of a Rails ActiveRecord model Product that has a has many association comments:
let(:product) { Product.new }
let(:comments) { [Comment.new(text: "Foo"), Comment.new(text: "Bar")] }
before :each do
allow_any_instnace_of(Product).to recieve(:comments).and_return(comments)
Now in any of your it blocks when you call product.comments you will get back an array of comments you can use in the tests without having gone near your database which makes the test orders of magnitudes faster.
When you are using the stub to check if the method was called the key is to declare the expectation before you perform the opreation that calls the method. For example:
expect_any_instance_of(Foo).to recieve(:bar).exactly(1).times.with('hello')
Foo.new.bar('hello') # will return true

Rspec for complicated case

I'm new to RSpec, and struggling with how to test with mock.
This is basically called when webhook comes in.
class InvoiceCreated
def call(event)
invoice = event.data.object
# NOTE: Skip if the invoice is closed.
if invoice.closed == false
stripe_customer = invoice.customer
payment_account = PaymentCardAccount.find_by(stripe_customer_id: stripe_customer)
card_invoice = Invoice.find_card_invoice_in_this_month_within(payment_account: payment_account)
card_invoice.process_invoice_items(stripe_customer: stripe_customer,
event_invoice_id: invoice.id)
card_invoice.process!(:pending, id: invoice.id)
end
end
end
I'd love to use mock and prevent API calling for testing for two lines of code below.
card_invoice.process_invoice_items(stripe_customer: stripe_customer,
event_invoice_id: invoice.id)
card_invoice.process!(:pending, id: invoice.id)
how can I use mock for those?
You can use expect_any_instance_of to check that you call Invoice#process_invoice_items and Invoice#process with the correct parameters. If you do not care about how they are called you can just stub them and use allow_any_instance_of for this.
expect_any_instance_of(Invoice).to receive(:process_invoice_items)
expect_any_instance_of(Invoice).to receive(:process!)
See here for more detailed examples and got through the basic section of rspec-mock to see what you can do with it actually.

Stubbing out ActiveRecord models in Service tests

I'm following a TDD approach to building our app, and creating a whole bunch of service objects, keeping models strictly for data management.
Many of the services I've built interface with models. Take for example MakePrintsForRunner:
class MakePrintsForRunner
def initialize(runner)
#runner = runner
end
def from_run_report(run_report)
run_report.photos.each do |photo|
Print.create(photo: photo, subject: #runner)
end
end
end
I appreciate the create method could arguably be abstracted into the Print model, but let's keep it as is for now.
Now, in the spec for MakePrintsForRunner I'm keen to avoid including spec_helper, since I want my service specs to be super fast.
Instead, I stub out the Print class like this:
describe RunnerPhotos do
let(:runner) { double }
let(:photo_1) { double(id: 1) }
let(:photo_2) { double(id: 2) }
let(:run_report) { double(photos: [photo_1, photo_2]) }
before(:each) do
#service = RunnerPhotos.new(runner)
end
describe "#create_print_from_run_report(run_report)" do
before(:each) do
class Print; end
allow(Print).to receive(:create)
#service.create_print_from_run_report(run_report)
end
it "creates a print for every run report photo associating it with the runners" do
expect(Print).to have_received(:create).with(photo: photo_1, subject: runner)
expect(Print).to have_received(:create).with(photo: photo_2, subject: runner)
end
end
end
And all goes green. Perfect!
... Not so fast. When I run the whole test suite, depending on the seed order, I am now running into problems.
It appears that the class Print; end line can sometimes overwrite print.rb's definition of Print (which obviously inherits from ActiveRecord) and therefore fail a bunch of tests at various points in the suite. One example is:
NoMethodError:
undefined method 'reflect_on_association' for Print:Class
This makes for an unhappy suite.
Any advice on how to tackle this. While this is one example, there are numerous times where a service is directly referencing a model's method, and I've taken the above approach to stubbing them out. Is there a better way?
You don't have to create the Print class, simply use the one that is loaded, and stub it:
describe RunnerPhotos do
let(:runner) { double }
let(:photo_1) { double(id: 1) }
let(:photo_2) { double(id: 2) }
let(:run_report) { double(photos: [photo_1, photo_2]) }
before(:each) do
#service = RunnerPhotos.new(runner)
end
describe "#create_print_from_run_report(run_report)" do
before(:each) do
allow(Print).to receive(:create)
#service.create_print_from_run_report(run_report)
end
it "creates a print for every run report photo associating it with the runners" do
expect(Print).to have_received(:create).with(photo: photo_1, subject: runner)
expect(Print).to have_received(:create).with(photo: photo_2, subject: runner)
end
end
end
Edit
If you really need to create the class in the scope of this test alone, you can undefine it at the end of the test (from How to undefine class in Ruby?):
before(:all) do
unless Object.constants.include?(:Print)
class TempPrint; end
Print = TempPrint
end
end
after(:all) do
if Object.constants.include?(:TempPrint)
Object.send(:remove_const, :Print)
end
end
I appreciate the create method could arguably be abstracted into the Print model, but let's keep it as is for now.
Let's see what happens if we ignore this line.
Your difficulty in stubbing a class is a sign that the design is inflexible. Consider passing an already-instantiated object to either the constructor of MakePrintsForRunner or the method #from_run_report. Which to choose depends on the permanence of the object - will the configuration of printing need to change at run time? If not, pass to the constructor, if so, pass to the method.
So for our step 1:
class MakePrintsForRunner
def initialize(runner, printer)
#runner = runner
#printer = printer
end
def from_run_report(run_report)
run_report.photos.each do |photo|
#printer.print(photo: photo, subject: #runner)
end
end
end
Now it's interesting that we're passing two objects to the constructor, yet #runner is only ever passed to the #print method of #printer. This could be a sign that #runner doesn't belong here at all:
class MakePrints
def initialize(printer)
#printer = printer
end
def from_run_report(run_report)
run_report.photos.each do |photo|
#printer.print(photo)
end
end
end
We've simplified MakePrintsForRunner into MakePrints. This only takes a printer at construction time, and a report at method invocation time. The complexity of which runner to use is now the responsibility of the new 'printer' role.
Note that the printer is a role, not necessarily a single class. You can swap the implementation for different printing strategies.
Testing should now be simpler:
photo1 = double('photo')
photo2 = double('photo')
run_report = double('run report', photos: [photo1, photo2])
printer = double('printer')
action = MakePrints.new(printer)
allow(printer).to receive(:print)
action.from_run_report(run_report)
expect(printer).to have_received(:print).with(photo1)
expect(printer).to have_received(:print).with(photo2)
These changes might not suit your domain. Perhaps a runner shouldn't be attached to a printer for more than one print. In this case, perhaps you should take a different next step.
Another future refactoring might be for #from_run_report to become #from_photos, since the report isn't used for anything but gathering photos. At this point the class looks a bit anaemic, and might disappear altogether (eaching over photos and calling #print isn't too interesting).
Now, how to test a printer? Integrate with ActiveRecord. This is your adapter to the outside world, and as such should be integration tested. If all it really does is create a record, I probably wouldn't even bother testing it - it's just a wrapper around an ActiveRecord call.
Class names are just constants so you could use stub_const to stub an undefined constant and return a double.
So instead of defining a class in your before(:each) block do this:
before(:each) do
stub_const('Print', double(create: nil))
#service.create_print_from_run_report(run_report)
end

Resources