How to write test Rspec for class method? - ruby-on-rails

I'm new to RSpec so this really give me a headache. I have a search by keyword or by category in Post model:
def self.search(search, category_id)
if search.strip.empty?
[]
elsif category_id.empty?
Post.approved.where('lower(title) LIKE ?', "%#{search.downcase.strip}%")
else
#category = Category.find_by('id = ?', category_id.to_i)
#category.posts.approved.where('lower(title) LIKE ?', "%#{search.downcase.strip}%")
end
end
I know how to write test for easy thing like validations and associations, but I still can not figure how to write test for this class method. Any help is appreciated.

You could create some test data:
let!(:post_1) { Post.create(title: 'some example title') }
let!(:post_2) { Post.create(title: 'another title') }
And validate that your search returns the correct records for various search terms, e.g.:
expect(Post.search('example')).to contain_exactly(post_1)
expect(Post.search('EXAMPLE')).to contain_exactly(post_1)
expect(Post.search('title')).to contain_exactly(post_1, post_2)
expect(Post.search('foo')).to be_empty
(assuming search is a method of Post)

Related

Monkey patching ActiveRecord "where" method

I'm trying to add some additional functionality to where method in ActiveRecord. I reached half way by doing monkey patch but facing problem with chain queries.
Ex:
User.where(id: 10, name: 'Blob')
When I execute above code my new functionality is working as expected.
It triggers modified where method and gives query params as
query_params = { id: 10, name: 'Blob' }
User.where(id: 10).where(name: 'Blob')
In this case i'm getting only
query_params = { id: 10 }
I'm just printing the query params and delegating to super class:
def where(query, *values)
query.each do |key, value|
if value.is_a?(Array) and value.flatten != value
Rails.logger.debug "Where Clause Params"
Rails.logger.debug "#{query}"
Rails.logger.debug "#{caller.join("\n")}"
break
end
end
super
end
This is how I extend modified where file into ApplicationRecord
ApplicationRecord.extend CoreExtensions::ApplicationRecord::WhereLog
It would be great if anyone helps.

Chaining ActiveRecord_Relation in PORO

In a Rails 5.1 app, I have a query object (PORO) named CoolProducts.
class CoolProducts
def self.call(relation = Product.all)
...
# return an instance of Product::ActiveRecord_Relation
end
end
Now I need to limit the found Products based on the fact the name matches a string.
The following works
CoolProducts.call.where("name ILIKE ?", "%#{string}%")
However, I'd like to encapsulate the matching login within the CoolProducts class allowing to do something like
CoolProducts.call.including_in_name(string)
But I'm not sure where to start from.
Any ideas?
It will be difficult if you want any of your methods to be chainable or return ActiveRecord::Relation.
If you consider explicitly fetching the records when you're done chaining being ok, this should work:
class CoolProducts
def initialize(relation)
#relation = relation
end
def self.call(relation = Product.all)
new(relation).apply_scopes
end
attr_reader :relation
alias_method :fetch, :relation
def including_in_name(string)
tap { #relation = relation.where("name ILIKE ?", string) }
end
def apply_scopes
tap { #relation = relation.where(price: 123) }
end
end
Usage:
CoolProducts.call.including_in_name(string).fetch

Calling a ActiveRecord class method for ActiveRecord_Relation as a receiver

I want to create a class method for a class inherits ActiveRecord:Base.
What the method need to do is add where clauses based on the options and it works well.
class Article < ActiveRecord::Base
def self.list_by_params(params={})
articles = self
articles = articles.where(author_id: params[:author_id]) unless params[:author_id].blank?
articles = articles.where(category_id: params[:category_id]) unless params[:category_id].blank?
articles = articles.where("created_at > ?", params[:created_at].to_date) unless params[:created_at].blank?
articles
end
end
This code works fine in case of the call such as:
articles = Article.list_by_params({author_id: 700})
#=> Works fine as I expected.
articles = Article.joins(:authors).list_by_params({author_id: 700})
#=> Works fine as I expected.
However, the problem is that, if I want to call the list_by_params without filtering params, then it lose its former relations. For example:
articles = Article.joins(:authors).list_by_params({})
#=> articles is just a `Article` (not an ActiveRecord_Relation) class itself without joining :authors.
Is there any chance that I made a mistake?
Thanks in advance.
What you are looking for is a scope.
I would do something like this
scope :for_author, lambda { |author| where(author_id: author) unless author.blank? }
scope :in_category, lambda { |category| where(category_id: category) unless category.blank? }
scope :created_after, lambda { |date| where('created_at > ?', date.to_date) unless date.blank? }
scope :list_by_params, lambda do |params|
for_author(params[:author_id])
.in_category(params[:category_id])
.created_after(params[:created_at])
end
Now you can reuse the components of your query. Everything has a names and it gets easier to read the code.
For the self explanation, I've solved the problems by using where(nil).
Actually, Model.scoped returned anonymous scope but the method has been deprecated since Rails version 4. Now, where(nil) can replace the functionality.
class Article < ActiveRecord::Base
def self.list_by_params(params={})
articles = where(nil) # <-- HERE IS THE PART THAT I CHANGED.
articles = articles.where(author_id: params[:author_id]) unless params[:author_id].blank?
articles = articles.where(category_id: params[:category_id]) unless params[:category_id].blank?
articles = articles.where("created_at > ?", params[:created_at].to_date) unless params[:created_at].blank?
articles
end
end

Rails 3 multiple parameter filtering using scopes

Trying to do a basic filter in rails 3 using the url params. I'd like to have a white list of params that can be filtered by, and return all the items that match. I've set up some scopes (with many more to come):
# in the model:
scope :budget_min, lambda {|min| where("budget > ?", min)}
scope :budget_max, lambda {|max| where("budget < ?", max)}
...but what's the best way to use some, none, or all of these scopes based on the present params[]? I've gotten this far, but it doesn't extend to multiple options. Looking for a sort of "chain if present" type operation.
#jobs = Job.all
#jobs = Job.budget_min(params[:budget_min]) if params[:budget_min]
I think you are close. Something like this won't extend to multiple options?
query = Job.scoped
query = query.budget_min(params[:budget_min]) if params[:budget_min]
query = query.budget_max(params[:budget_max]) if params[:budget_max]
#jobs = query.all
Generally, I'd prefer hand-made solutions but, for this kind of problem, a code base could become a mess very quickly. So I would go for a gem like meta_search.
One way would be to put your conditionals into the scopes:
scope :budget_max, lambda { |max| where("budget < ?", max) unless max.nil? }
That would still become rather cumbersome since you'd end up with:
Job.budget_min(params[:budget_min]).budget_max(params[:budget_max]) ...
A slightly different approach would be using something like the following inside your model (based on code from here:
class << self
def search(q)
whitelisted_params = {
:budget_max => "budget > ?",
:budget_min => "budget < ?"
}
whitelisted_params.keys.inject(scoped) do |combined_scope, param|
if q[param].nil?
combined_scope
else
combined_scope.where(whitelisted_params[param], q[param])
end
end
end
end
You can then use that method as follows and it should use the whitelisted filters if they're present in params:
MyModel.search(params)

Moching rails association methods

Here is my helper method which I want to test.
def posts_correlation(name)
if name.present?
author = User.find_by_name(name)
author.posts.count * 100 / Post.count if author
end
end
A factory for user.
factory :user do
email 'user#example.com'
password 'secret'
password_confirmation { password }
name 'Brian'
end
And finally a test which permanently fails.
test "should calculate posts count correlation" do
#author = FactoryGirl.create(:user, name: 'Jason')
#author.posts.expects(:count).returns(40)
Post.expects(:count).returns(100)
assert_equal 40, posts_correlation('Jason')
end
Like this.
UsersHelperTest:
FAIL should calculate posts count correlation (0.42s)
<40> expected but was <0>.
test/unit/helpers/users_helper_test.rb:11:in `block in <class:UsersHelperTest>'
And the whole problem is that mocha doesn't really mock the count value of author's posts, and it returns 0 instead of 40.
Are there any better ways of doing this: #author.posts.expects(:count).returns(40) ?
When your helper method runs, it's retrieving its own object reference to your author, not the #author defined in the test. If you were to puts #author.object_id and puts author.object_id in the helper method, you would see this problem.
A better way is to pass the setup data for the author in to your mocked record as opposed to setting up expectations on the test object.
It's been a while since I used FactoryGirl, but I think something like this should work:
#author = FactoryGirl.create(:user, name: 'Jason')
(1..40).each { |i| FactoryGirl.create(:post, user_id: #author.id ) }
Not terribly efficient, but should at least get the desired result in that the data will actually be attached to the record.

Resources