I'm using rspec for testing. I have this piece of code:
class Service
def execute
all_users.update_all(status: 'deactive')
end
def all_users
#all_users ||= User.status_active
end
end
Then I have the following two expectations:
expect(service.all_users.count).to eq 10
service.execute
expect(service.all_users.count).to eq 0
They both return true. It means that the first time I call all_users, it is evaluated once. And on the second call, all_users is evaluated again, this time because I have changed all user's state to deactive and the total active users is zero.
The ||= operator evaluates the code for the variable only on the first time. Why is my code above evaluated again?
What Andry said is true; the value saved in #all_users is not just an array or list, it's an ActiveRecord relation. When you call all_users.count, it will make a db query to determine the result.
I suspect User.all_users is either a scope or a class method which does something like where(status: 'active').
In such case, User.all_users returns not a collection of models, but a lazily evaluated SQL query. When you write User.status_active, you actually make a new SQL query and get actual data
Related
If I have a User model that includes a method dangerous_action and somewhere I have code that calls the method on a specific subset of users in the database like this:
class UserDanger
def perform_dangerous_action
User.where.not(name: "Fred").each(&:dangerous_action)
end
end
how do I test with RSpec whether it's calling that method on the correct users, without actually calling the method?
I've tried this:
it "does the dangerous thing, but not on Fred" do
allow_any_instance_of(User).to receive(:dangerous_action).and_return(nil)
u1 = FactoryBot.create(:user, name: "Jill")
u2 = FactoryBot.create(:user, name: "Fred")
UserDanger.perform_dangerous_action
expect(u1).to have_recieved(:dangerous_action)
expect(u2).not_to have_recieved(:dangerous_action)
end
but, of course, the error is that the User object doesn't respond to has_recieved? because it's not a double because it's an object pulled from the database.
I think I could make this work by monkey-patching the dangerous_action method and making it write to a global variable, then check the value of the global variable at the end of the test, but I think that would be a really ugly way to do it. Is there any better way?
I realised that I'm really trying to test two aspects of the perform_dangerous_action method. The first is the scoping of the database fetch, and the second is that it calls the correct method on the User objects that come up.
For testing the scoping of the DB fetch, I should really just make a scope in the User class:
scope :not_fred, -> { where.not(name: "Fred") }
which can be easily tested with a separate test.
Then the perform_dangerous_action method becomes
def perform_dangerous_action
User.not_fred.each(&:dangerous_action)
end
and the test to check it calls the right method for not_fred users is
it "does the dangerous thing" do
user_double = instance_double(User)
expect(user_double).to receive(:dangerous_action)
allow(User).to receive(:not_fred).and_return([user_double])
UserDanger.perform_dangerous_action
end
i think, in many cases, you don't want to separate a where or where.not into a scope, in that cases, you could stub ActiveRecord::Relation itself, such as:
# default call_original for all normal `where`
allow_any_instance_of(ActiveRecord::Relation)
.to receive(:where).and_call_original
# stub special `where`
allow_any_instance_of(ActiveRecord::Relation)
.to receive(:where).with(name: "...")
.and_return(user_double)
in your case, where.not is actually call ActiveRecord::QueryMethods::WhereChain#not method so i could do
allow_any_instance_of(ActiveRecord::QueryMethods::WhereChain)
.to receive(:not).with(name: "Fred")
.and_return(user_double)
How could I write a test to find the last created record?
This is the code I want to test:
Post.order(created_at: :desc).first
I'm also using factorybot
If you've called your method 'last_post':
def self.last_post
Post.order(created_at: :desc).first
end
Then in your test:
it 'should return the last post' do
expect(Post.last_post).to eq(Post.last)
end
On another note, the easiest way to write your code is simply
Post.last
And you shouldn't really be testing the outcome of ruby methods (you should be making sure the correct ruby methods are called), so if you did:
def self.last_post
Post.last
end
Then your test might be:
it 'should send the last method to the post class' do
expect(Post).to receive(:last)
Post.last_post
end
You're not testing the outcome of the 'last' method call - just that it gets called.
The accepted answer is incorrect. Simply doing Post.last will order the posts by the ID, not by when they were created.
https://apidock.com/rails/ActiveRecord/FinderMethods/last
If you're using sequential IDs (and ideally you shouldn't be) then obviously this will work, but if not then you'll need to specify the column to sort by. So either:
def self.last_post
order(created_at: :desc).first
end
or:
def self.last_post
order(:created_at).last
end
Personally I'd look to do this as a scope rather than a dedicated method.
scope :last_created -> { order(:created_at).last }
This allows you to create some nice chains with other scopes, such as if you had one to find all posts by a particular user/account, you could then chain this pretty cleanly:
Post.for_user(user).last_created
Sure you can chain methods as well, but if you're dealing with Query interface methods I feel scopes just make more sense, and tend to be cleaner.
If you wanted to test that it returns the correct record, in your test you could do something like:
let!(:last_created_post) { factory_to_create_post }
. . .
it "returns the correct post"
expect(Post.last_post).to eq(last_created_post)
end
If you wanted to have an even better test, you could create a couple records before the last record to verify the method under test is pulling the correct result and not just a result from a singular record.
Not sure if I'm reading Puma logs right. So I'd like to clarify three things:
First
If in controller we do something like:
def find_user
user = User.where(name: "Alex")
#user = user.where(age: 25)
end
How many sql queries are made?
Second
def find_user
user = User.where(name: "Alex")
#user = user
end
The same question.
Third
def find_user
user = User.find(1).id
#user = User.find(1).first_name
end
The same question.
While the third piece of code generate 2 DB queries, the first and second, technically speaking, generate 0. Any query may be generated if you use #user variable or the return value of find_user method (which is the same thing, obviously).
The reason behind this behavior is that ActiveRecord uses lazy evaluation of the queries, so they are performed by the time you need their results.
You can often be misguided by the console output, where all of these where calls, if you execute them one by one, generate SQL query, but it's only because under the hood the console calls inspect on every evaluated value, to generate output, and that's where ActiveRecord gets the message, 'oh, I need to actually perform DB operation to get the results needed'. Consider this example, you can check it in your console:
lambda do
user = User.where(name: "Alex")
#user = user.where(age: 25)
return 'any other value'
end.call
You'll get the 'any other value' string returned from this lambda expression, but guess what - the #user variable will be set to hold ActiveRecord::Relation instance and there would be 0 DB queries generated.
In Ruby on Rails ActiveRecord no SQL query will be made until you use ActiveRecord's object to fetch some record or execute some operation which will effect your DB.
So in first case there will be only 1 query to database same as second case. But in third case if you are using both user and #user to fetch record then two queries will be fired as both are different object. If you use only #user then only one query will be fired
With the new attribute marked_deleted in AssistantTeacher class,
We want all queries to work with the basic assumption of only selection where marked_deleted attribute is false.
This is easy (using squeel query language instead of standard AR query language)
class AssistantTeacher < ActiveRecord.Base
# Gives a default query which always includes the condition here
default_scope {where{marked_deleted == false }}
end
This means that queries like AssistantTeacher.all will actually be
AssistantTeacher.all.where{marked_deleted == false}
Works great.
Likewise AssistantTeacher.find_each() also works with the limitation.
likewise
AssistantTeacher.where{atcode == "MPL"}
also executes as
I
However then the tricky part:
We need to reverse the default_scope for admins etc in special cases:
class AssistantTeacher < ActiveRecord.Base
# Gives a default query which always includes the condition here
default_scope {where{marked_deleted == false }}
# If a query is unscoped then the default_scope limitation does not apply
# scoped is the default scope when there is no default_scope
unscoped { scoped}
end
This works fine for
def self.all_including_marked_deleted
return unscoped{all}
end
However: the question
I can't figure out how to do an unscoped for an admin version of find_each with a block
def self.find_each_including_marked_deleted &block
return unscoped{find_each(block)}
end
DOESNOT work. Nor do any other combination with block that I can think of.
Anyone any ideas what I can do to get my overriding method find_each_including_marked_deleted to pass its block to the unscoped call?
You just have a small syntax problem. If you add a & to the front of your block, you should be fine:
def self.find_each_including_marked_deleted &block
unscoped { find_each &block }
end
What's going on here? Inside the method body, the block variable is an instance of Proc (that's what the & is doing in front of the block param). You can verify this by checking block.class. find_each takes a block, not a proc, so you need the & to convert the proc back to a block.
Clear as mud? You can read more about it here.
Based on the Rails 3 API, the difference between a scope and a class method is almost non-existent.
class Shipment < ActiveRecord::Base
def self.unshipped
where(:shipped => false)
end
end
is the same as
scope :unshipped, where(:shipped => false)
However, I'm finding that I'm sometimes getting different results using them.
While they both generate the same, correct SQL query, the scope doesn't always seem to return the correct values when called. It looks like this problem only occurs when its called the same way twice, albeit on a different shipment, in the method. The second time it's called, when using scope it returns the same thing it did the first time. Whereas if I use the class method it works correctly.
Is there some sort of query caching that occurs when using scope?
Edit:
order.line_items.unshipped
The line above is how the scope is being called. Orders have many line_items.
The generate_multiple_shipments method is being called twice because the test creates an order and generates the shipments to see how many there are. It then makes a change to the order and regenerates the shipments. However, group_by_ship_date returns the same results it did from the first iteration of the order.
def generate_multiple_shipments(order)
line_items_by_date = group_by_ship_date(order.line_items.unshipped)
line_items_by_date.keys.sort.map do |date|
shipment = clone_from_order(order)
shipment.ship_date = date
line_items_by_date[date].each { |line_item| shipment.line_items << line_item }
shipment
end
end
def group_by_ship_date(line_items)
hash = {}
line_items.each do |line_item|
hash[line_item.ship_date] ||= []
hash[line_item.ship_date] << line_item
end
hash
end
I think your invocation is incorrect. You should add so-called query method to execute the scope, such as all, first, last, i.e.:
order.line_items.unshipped.all
I've observed some inconsistencies, especially in rspec, that are avoided by adding the query method.
You didn't post your test code, so it's hard to say precisely, but my exeprience has been that after you modify associated records, you have to force a reload, as the query cache isn't always smart enough to detect a change. By passing true to the association, you can force the association to reload and the query to re-run:
order.line_items(true).unshipped.all
Assuming that you are referencing Rails 3.1, a scope can be affected by the default scope that may be defined on your model whereas a class method will not be.