Rails/Rspec Stubs: Specifying order & so forth - ruby-on-rails

I'm comfortable creating basic stubs, but a little confused about how to specify things such as order (and reversing it), etc.
To give a concrete example, here is a line in my controller that I'm trying to test:
#courses = Course.order('created_at').reverse
In my controller spec, the (obviously failing) stub is:
Course.stub(:all) { [mock_course] }
...and the rspec error:
Failure/Error: assigns(:courses).should eq([mock_course])
expected [#<Course:0x818614e8 #name="Course_1001">]
got [#<Course id: 2, name: "Second test course", price: #<BigDecimal:1030e73c0,'0.4995E2',18(18)>]
Despite being an inaccurate test (not testing ordering), i would have guessed the spec would pass. It doesn't - it's pulling from the database, not the mock. Soo...I guess what I'm asking is, how do I stub Course.order('created_at').reverse ?
Many thanks...

You're assuming that ActiveRecord will call Course.all at some point, which may not be the case.
Try this:
Course.stub_chain(:order, :reverse) { [mock_course] }
To avoid having to do chained stubbing, you could move the ActiveRecord code into your model, so the controller would have something simpler such as:
Course.all_by_newest_first

Related

One of my tests fails due to some issues with using let! in rspec

I have no clue what is wrong with the following:
let!(:account1) { create(:account) }
describe ".active_between" do
subject { Account.active_between(Date.today - 10.days, Date.today).to_a }
it { is_expected.to eq([account1]) }
end
Somehow this tests fails like this:
expected: [#<Account id: 2, lead_id: 2]
got: [#<Account id: 2, lead_id: 3]
What could be causing the issue with the lead_id that's being changed? There's absolutely no code repsonsible for changing it.
The Factory Bot is set-up like this:
factory :account do
association :lead
end
The lead factory is defined as:
factory :lead do
name 'Lead'
end
Do you have any other tests that change account1, and in particular it's lead in anyway? Rspec will run tests in random order so it could be a test later on that is causing it.
Regardless of that, these sorts of tests can be somewhat flaky. Do you really want to compare the entire object or just that the IDs match? If ID match is enough then your'e test is fine and you don't have to worry about other attributes (particularly timestamps being off a smidgen)
There is not enough information here unfortunately. Does it fail if you run ONLY this test?
If not, I would suggest using https://github.com/DatabaseCleaner/database_cleaner
to solve the problem and ensure that each test starts with clean database.
If that won't help, there must be something in models classes that interferes with it. If you encounter further problems, add also snippet from both models, we probably will be able to tell you more then

How does Rspec 'let' helper work with ActiveRecord?

It said here https://www.relishapp.com/rspec/rspec-core/v/3-5/docs/helper-methods/let-and-let what variable defined by let is changing across examples.
I've made the same simple test as in the docs but with the AR model:
RSpec.describe Contact, type: :model do
let(:contact) { FactoryGirl.create(:contact) }
it "cached in the same example" do
a = contact
b = contact
expect(a).to eq(b)
expect(Contact.count).to eq(1)
end
it "not cached across examples" do
a = contact
expect(Contact.count).to eq(2)
end
end
First example passed, but second failed (expected 2, got 1). So contacts table is empty again before second example, inspite of docs.
I was using let and was sure it have the same value in each it block, and my test prove it. So suppose I misunderstand docs. Please explain.
P.S. I use DatabaseCleaner
P.P.S I turn it off. Nothing changed.
EDIT
I turned off DatabaseCleaner and transational fixtures and test pass.
As I can understand (new to programming), let is evaluated once for each it block. If I have three examples each calling on contact variable, my test db will grow to three records at the end (I've tested and so it does).
And for right test behevior I should use DatabaseCleaner.
P.S. I use DatabaseCleaner
That's why your database is empty in the second example. Has nothing to do with let.
The behaviour you have shown is the correct behaviour. No example should be dependant on another example in setting up the correct environment! If you did rely on caching then you are just asking for trouble later down the line.
The example in that document is just trying to prove a point about caching using global variables - it's a completely different scenario to unit testing a Rails application - it is not good practice to be reliant on previous examples to having set something up.
Lets, for example, assume you then write 10 other tests that follow on from this, all of which rely on the fact that the previous examples have created objects. Then at some point in the future you delete one of those examples ... BOOM! every test after that will suddenly fail.
Each test should be able to be tested in isolation from any other test!

How to test a method on an ActiveRecord::Relation object in rspec?

How do I test a method available only to an ActiveRecord relation proxy class in rspec? Like for example sum which would look something like #collection.sum(:attribute)
Here is what I'm trying to do:
#invoice = stub_model(Invoice)
#line_item = stub_model(LineItem, {quantity: 1, cost: 10.00, invoice: #invoice})
#invoice.stub(:line_items).and_return([#line_item])
#invoice.line_items.sum(:cost).should eq(10)
This doesn't work because #invoice.line_items returns a regular array that doesn't define sum in the same way as an ActiveRecord::Relation object does.
Any help is greatly appreciated.
I'm not sure which Rails you are on so I'll use Rails 4.0.x for this example; the principle still holds for Rails 3.x.
TL;DR: You don't want to take this route.
Consider not stubbing model specs
Consider adding domain specific APIs
You are rapidly heading down the road of over mocking/stubbing. I have been down this road, it does not lead to fun. Part of all of this comes down to violating the Law of Demeter. Part of it comes down to using the Rails APIs instead of creating your own domain APIs.
When you request an relation collection from an ActiveRecord model it does not return an Array as you are aware. In Rails 4.0.x, with a has_many association, the class which is returned is: ActiveRecord::Associations::CollectionProxy::ActiveRecord_Associations_CollectionProxy_Model.
Issue #1: Stubbing the wrong return value
Here your return type is an Array. While the actual return type is the ActiveRecord_Associations_CollectionProxy_Model. In stub/mock land, this isn't necessarily a bad thing. However, if you intend to use other calls on the object returned by the stub they need to match the same API contracts. Otherwise, you're not stubbing the same behavior.
In this case, the sum method defined on the AR association proxy actually executes SQL when it runs. The sum method defined on Array is patched in via Active Support. The Array#sum behavior is fundamentally different:
def sum(identity = 0, &block)
if block_given?
map(&block).sum(identity)
else
inject { |sum, element| sum + element } || identity
end
end
As you can see, it sums the elements, not the sum of the requested attribute.
Issue #2: Asserting on your stub'd object
The other main problem you have, is you are attempting to spec that you're stub returns what you stubbed. This doesn't make sense. The point of a stub is to return a canned answer. It's not to assert on how it behaves.
What you wrote isn't fundamentally different from:
invoice = stub_model(Invoice)
line_item = stub_model(LineItem, {quantity: 1, cost: 10.00, invoice: invoice})
invoice.stub(:line_items).and_return([line_item])
invoice.line_items.should eq([line_item])
Unless this is supposed to be a sanity check, it adds no real value to your specs.
Suggestions
I'm not sure what type of spec you are writing here. If this is a more traditional unit test or an acceptance test, then I probably wouldn't stub anything. There isn't necessarily anything wrong with hitting a database at times, especially when the thing you are testing is how you interact with it; which is really what you are doing here.
Another thing you can do is start to use this to create your own specific domain model APIs. All this really means is defining interfaces on objects that make sense for your domain, which may or may not be backed by a DB or other resource.
For example, take your invoice.line_items.sum(:cost).should eq(10), this is clearly testing the Rails AR API. In domain terms it means nothing really. However, invoice.subtotal probably means a lot more to your domain:
# app/models/invoice.rb
class Invoice < ActiveRecord::Base
def subtotal
line_items.sum(:cost)
end
end
# spec/models/invoice_spec.rb
# These are unit specs on the model, which directly works with the DB
# it probably doesn't make sense to stub things here
describe Invoice do
specify "the subtotal is the sum of all line item cost" do
invoice = create(:invoice)
3.times do |i|
cost = (i + 1) * 2
invoice.line_items.create(cost: cost)
end
expect(invoice.subtotal).to eq 12
end
end
Now later, when you use Invoice in some other part of your code, you can easily stub this if you need to:
# spec/helpers/invoice_helper_spec.rb
describe InvoiceHelper do
context "requesting the formatted subtotal" do
it "returns US dollars to two decimal places" do
invoice = double(Invoice, subtotal: 1012)
assign(:invoice, invoice)
expect(helper.subtotal_in_dollars).to eq "$10.12"
end
end
end
So when it is ok to stub model specs? Well, that's really a judgement call, and will vary from person to person, and code base to code base. However, just because something is in app/models doesn't mean it has to be an ActiveRecord model. In those cases, it's potentially fine to stub domain APIs on collaborators.
EDIT: create vs build
In the example above I used create(:invoice) and invoice.line_items.create(cost: cost). However, if you are concerned about DB slowness, you probably could just as easily use build(:invoice) and invoice.line_items.build(cost: cost).
Be aware that my use of create(:invoice) and build(:invoice) here is in reference to generic "factories", not a reference to a specific gem. You could simply use Model.create and Model.new in their place. Additionally, the line_items.create and line_items.build are provided by AR and have nothing to do with any factory gems.

RSpec: Test Scope Eager Loading

One of my models' default scope is set to automatically load a related model with includes(). How can I test this functionality with RSpec? I tried running:
Model.includes(:relation).to_sql
Hoping I could detect the signature in the SQL, but it only returned this:
SELECT "models".* FROM "models"
No mention of the relation, it's apparently done with a second query. How would you recommend testin this?
I would probably go down the route of stubbing the method and setting an expectation.
The pattern would be to add a test double of your model and set the expectation that it receives the .includes method call
Testing the actual SQL call created by Rails seems to be more about testing Rails rather than your app and tightly couples it to the implementation. Testing that the method gets called is a good middle ground, with the knowledge that if it gets called, Rails will do the right thing.
There is a lot more information here https://www.relishapp.com/rspec/rspec-mocks/v/2-14/docs/method-stubs to better understand the process.
includes(:relation) will add a left outer join relations to your query. This means you can use relations in your query without having to joins(:relations).
let!(:model) {
Model.create!( field: 42 )
}
let!(:relation) {
model.relations.create!( this: "that")
}
it 'includes relation' do
expect( Model.where("relations.this": "that") ).to contain_exactly(relation)
end

How to write Rspec spec for the following query

Hi I have the following query in my controller and I want to write the Rspec spec . I am new to Rspec and I don't know how to write the spec. Kindly help
table1.includes(:table2).where(table1: {id: params[:id]}).includes(:table3)
I also tried looking into mocks and stubs but i don't understand how to use them for a query like this.
Thanks
When faced with these issues, I tend to encapsulate the query in a method. That way, you can stub out the method with data simply and without worrying about data-sanitation.
For example:
def fetch_table1_results(id)
table1.includes(:table2).where(table1: {id: id}).includes(:table3)
end
At this point, you can stub out the method when you need to test things that depend on it:
awesome_model = stub_model(Table1, fetch_table1_results: [1, 2, 'etc']) # You should include models, stubs, or mocks here.
As far as testing the actual method, I'm not sure you need to. There aren't many interesting parts of that method chain. If you wanted to be complete, here are the cases:
Ensure fetch_table1_results calls any instance of Table1.find with id
Ensure fetch_table1_results eager-loads table2 and table3
The way of doing the latter varies, but I'm rather fond (and this won't be a popular opinion) of checking the database query directly. So you could type something like the following:
fetch_table1_results(1).to_sql.should include('JOIN table2')
That, or something similar. I should also note that these tests should be in the model, not the controller.

Resources