How to use dom_id on non-saved/mock objects in Rails? - ruby-on-rails

Note: I'm only posting this question so that others may find it if they ever need to, as I have found a good solution.
In my controller tests, I don't want to commit to the database, but I still want the controllers to use finder methods to get mock objects (by mocking the find method), but then I want to use dom_id on those mocks in assert_select, to verify that they're being displayed.
However, since they're non-saved objects, dom_id keeps returning new_object instead of object_1, object_2, et cetera.
Is there any quick way to get it to work? I really don't want to persist real records in tests.

Suppose you're using ActiveSupport::TestCase and FactoryGirl for building model objects, you can add this to your test/test_helper.rb file:
# Generate model with id - useful for assert_select with dom_id
def model(name, options = {})
#next_id = {} if #next_id.nil?
#next_id[name] = 1 unless #next_id.has_key(name)
m = build(name, options)
m.id = #next_id[name]
#next_id[name] += 1
m
end
Then, suppose you're using RR as your mocking library, you can do something like this:
test 'something' do
posts = [model(:post), model(:post)]
mock(Post).all { posts }
get :index
assert_select "##{dom_id(posts.first)}", posts.first.title
end

Related

Find last created record RSpec test

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.

Rails; Fetch records within initializer

I've been wondering it is common to fetch records within initializer?
Here this is an example for service object to fetch records and generated pdf receipt file.
Input is invoice uuid, and fetch the related records such as card detail, invoice items within initialier.
class Pdf::GenerateReceipt
include Service
attr_reader :invoice, :items, :card_detail
def initialize(invoice_uuid)
#invoice ||= find_invoice!(invoice_uuid) # caching
#items = invoice.invoice_items
#card_detail = card_detail
end
.....
def call
return ReceiptGenerator.new(
id: invoice.uuid, # required
outline: outline, # required
line_items: line_items, # required
customer_info: customer_info
)
rescue => e
false, e
end
.....
def card_detail
card_metadata = Account.find(user_detail[:id]).credit_cards.primary.last
card_detail = {}
card_detail[:number] = card_metadata.blurred_number
card_detail[:brand] = card_metadata.brand
card_detail
end
end
Pdf::GenerateReceipt.('28ed7bb1-4a3f-4180-89a3-51cb3e621491') # => then generate pdf
The problem is if the records not found, this generate an error.
I could rescue within the initializer, however that seems not common.
How could I work around this in more ruby way?
This is mostly opinion and anecdotal, but I prefer to deal with casting my values as far up the chain as possible. So i would find the invoice before this object and pass it in as an argument, same with the card_detail.
If you do that in this class, it will limit the responsibility to coordinating those two objects, which is way easier to test but also adds another layer that you have to reason about in the future.
So how i would handle, split this into 4 separate things
Invoice Finder thing
Card Finder thing
Pdf Generator that takes invoice and card as arguments
Finally, something to orchestrate the 3 actions above
Hope this helps.
Addition: Check out the book confident ruby by avdi grimm. It's really great for outlining handling this type of scenario.

Can I make Rails update_attributes with nested form find existing records and add to collections instead of creating new ones?

Scenario: I have a has_many association (Post has many Authors), and I have a nested Post form to accept attributes for Authors.
What I found is that when I call post.update_attributes(params[:post]) where params[:post] is a hash with post and all author attributes to add, there doesn't seem to be a way to ask Rails to only create Authors if certain criteria is met, e.g. the username for the Author already exists. What Rails would do is just failing and rollback update_attributes routine if username has uniqueness validation in the model. If not, then Rails would add a new record Author if one that does not have an id is in the hash.
Now my code for the update action in the Post controller becomes this:
def update
#post = Post.find(params[:id])
# custom code to work around by inspecting the author attributes
# and pre-inserting the association of existing authors into the testrun's author
# collection
params[:post][:authors_attributes].values.each do |author_attribute|
if author_attribute[:id].nil? and author_attribute[:username].present?
existing_author = Author.find_by_username(author_attribute[:username])
if existing_author.present?
author_attribute[:id] = existing_author.id
#testrun.authors << existing_author
end
end
end
if #post.update_attributes(params[:post])
flash[:success] = 'great!'
else
flash[:error] = 'Urgg!'
end
redirect_to ...
end
Are there better ways to handle this that I missed?
EDIT: Thanks for #Robd'Apice who lead me to look into overriding the default authors_attributes= function that accepts_nested_attributes_for inserts into the model on my behalf, I was able to come up with something that is better:
def authors_attributes=(authors_attributes)
authors_attributes.values.each do |author_attributes|
if author_attributes[:id].nil? and author_attributes[:username].present?
author = Radar.find_by_username(radar_attributes[:username])
if author.present?
author_attributes[:id] = author.id
self.authors << author
end
end
end
assign_nested_attributes_for_collection_association(:authors, authors_attributes, mass_assignment_options)
end
But I'm not completely satisfied with it, for one, I'm still mucking the attribute hashes from the caller directly which requires understanding of how the logic works for these hashes (:id set or not set, for instance), and two, I'm calling a function that is not trivial to fit here. It would be nice if there are ways to tell 'accepts_nested_attributes_for' to only create new record when certain condition is not met. The one-to-one association has a :update_only flag that does something similar but this is lacking for one-to-many relationship.
Are there better solutions out there?
This kind of logic probably belongs in your model, not your controller. I'd consider re-writing the author_attributes= method that is created by default for your association.
def authors_attributes=(authors_attributes)
authors_attributes.values.each do |author_attributes|
author_to_update = Author.find_by_id(author_attributes[:id]) || Author.find_by_username(author_attributes[:username]) || self.authors.build
author_to_update.update_attributes(author_attributes)
end
end
I haven't tested that code, but I think that should work.
EDIT: To retain the other functionality of accepts_nested_Attributes_for, you could use super:
def authors_attributes=(authors_attributes)
authors_attributes.each do |key, author_attributes|
authors_attributes[key][:id] = Author.find_by_username(author_attributes[:username]).id if author_attributes[:username] && !author_attributes[:username].present?
end
super(authors_attributes)
end
If that implementation with super doesn't work, you probably have two options: continue with the 'processing' of the attributes hash in the controller (but turn it into a private method of your controller to clean it up a bit), or continue with my first solution by adding in the functionality you've lost from :destroy => true and reject_if with your own code (which wouldn't be too hard to do). I'd probably go with the first option.
I'd suggest using a form object instead of trying to get accepts_nested_attributes to work. I find that form object are often much cleaner and much more flexible. Check out this railscast

RSpec Rails Test Data

I'm trying to optimise my specs a bit. I usually have a problem creating test data for nested resources and users. I usually end up with a before(:each) that sets up the data, this is run by more than 120 of my specs. Let me show you: (it's not accurate, but you should get the point)
def setup_test_data
#user = FactoryGirl.create(:admin_with_account)
#account = #user.account
3.times do |n|
list = FactoryGirl.create(:list)
list.items << FactoryGirl.create_list(:item, 3)
#account.lists << list
end
end
before(:each){setup_test_data}
subject{List.merge(list1, list2)}
it{should have(6).items}
And here is why I fail to shorten my test data setup
def self.merge(lists)
merged_list = lists.first.account.subscriber_lists.build
name = "Merge of "
lists.each do |list|
name << "'#{list.name}', "
list.items.each do |item|
merged_list.items.build(item.dup.attributes)
end
end
merged_list.name = name.chop.chop
merged_list.save!
merged_list.reload # I use this to filter out duplicates via validations
end
My Options:
A) move some logic back into the controller, less dependency on the account, save in the controller
B) stub/mock a lot more, but with nested resources + associations it's hard to do
C) your idea here:
Thanks
Ray
C) Create your own RSpec rake task that will first import some basic data, then use DatabaseCleaner to make sure everything runs transactionally (you will have to manually clear the DB after your custom rake tasks, because for some reason it doesn't seem to be, but with DatabaseCleaner this is a one-liner).
I use this in a situation where I have a large pre-defined dataset that I need to test against and want it to be created once, then have tests performed transactionally against it.
If this appeals to you, let me know and I can provide more code to help you out.

How should I test this, do I mock since it is hitting the db?

I have a method that does this:
Given a array of tags, it tries to fetch the Tag by its name from the db if it exists, otherwise it creates a new Tag. It then returns a collection of all the tag objects.
tag_text = "general tag1 tag2"
the Tag.rb model has a name property that would be in the db if it exists.
How should I go about testing the method:
def find_tags_from_tag_text (tag_text)
tags = []
tag_text.split.each do |x|
t = Tag.find_by_name(x)
if t.nil?
t = Tag.new(name => x).save!
end
tags << t
end
end
Rails testing guide http://guides.rubyonrails.org/testing.html covers pretty much what you are trying to do, have you checked it out? TL;DR: 1) prepare fixtures - data to initialize blank testing database with, 2) write tests to compare actual results with expected results of your model's methods.

Resources