Elegant way to test that new objects are created - ruby-on-rails

I'm building a very basic test to check if an object is created in a relationship. Website has_many languages. I've the method add_language that receives an array of ids and adds them.
This is some of my tests:
describe '#add_languages' do
subject{ build(:website) }
it 'return nil if no language is added' do
expect(subject.add_languages []).to be_nil
end
it 'adds one single language' do
languages_ids = [Language.last.id]
original_count = subject.languages.count
subject.add_languages languages_ids
expect(subject.languages.count).to eq(original_count + languages_ids.count)
end
it 'adds multiple languages' do
languages_ids = Language.limit(3).pluck :id
original_count = subject.languages.count
subject.add_languages languages_ids
expect(subject.languages.count).to eq(original_count + languages_ids.count)
end
end
What I don't like is
1) The code is duplicated
2) I don't like using eq for this, but not sure how to use other matchers
What would be an elegant way to write those tests?

Two quick ways you can do this: add a helper method, or add a loop.
Helper Method
it "adds on single language" do
check_creates_languages([Language.last.id])
end
it "Adds multiple languages" do
check_creates_languages(...)
end
def check_creates_languages(language_ids)
original_count = ... # etc
end
Loop
[1,3].each do |language_count|
it "can add #{language_count} language(s)" do
original_count = Language.last(language_count).count
# etc
end
end
Also, there's nothing wrong with using eq, that's what it's there for. What else would you want to use? You could also use be.
https://github.com/rspec/rspec-expectations

Simply you can do:
expect{subject.add_languages languages_ids}.to change{Language.count}.by(1)
More or less (based on your use case).

Related

How do I 'expect' a chain of methods using Rspec where the first method takes a parameter?

I have a method call in a ruby model that looks like the following:
Contentful::PartnerCampaign.find_by(vanityUrl: referral_source).load.first
Within the models spec.rb file, I'm trying to mock that call and get a value by passing in a param. But I'm having trouble figuring out the correct way of calling it.
At the top of my spec.rb file I have:
let(:first_double) {
double("Contentful::Model", fields {:promotion_type => "Promotion 1"})
}
Within the describe block I've tried the following:
expect(Contentful::PartnerCampaign).to receive_message_chain(:find_by, :load, :first).
and_return(first_double)
expect(Contentful::PartnerCampaign).to receive_message_chain(:find_by, :load, :first).with(vanityUrl: 'test_promo_path').
and_return(first_double)
expect(Contentful::PartnerCampaign).to receive_message_chain(:find_by => vanityUrl: 'test_promo_path', :load, :first).
and_return(first_double)
As you can probably guess, none of these are working. Does anyone know the correct way to do this sort of thing? Is it even possible?
Generally speaking, I prefer not to use stub chains, as they are often a sign that you are violating the Law of Demeter. But, if I had to, this is how I would mock that sequence:
let(:vanity_url) { 'https://vanity.url' }
let(:partner_campaigns) { double('partner_campaigns') }
let(:loaded_partner_campaigns) { double('loaded_partner_campaigns') }
let(:partner_campaign) do
double("Contentful::Model", fields {:promotion_type => "Promotion 1"}
end
before do
allow(Contentful::PartnerCampaign)
.to receive(:find_by)
.with(vanity_url: vanity_url)
.and_return(partner_campaigns)
allow(partner_campaigns)
.to receive(:load)
.and_return(loaded_partner_campaigns)
allow(loaded_partner_campaigns)
.to receive(:first)
.and_return(partner_campaign)
end
This is what I would do. Notice that I split the "mocking" part and the "expecting" part, because usually I'll have some other it examples down below (of which then I'll need those it examples to also have the same "mocked" logic), and because I prefer them to have separate concerns: that is anything inside the it example should just normally focus on "expecting", and so any mocks or other logic, I normally put them outside the it.
let(:expected_referral_source) { 'test_promo_path' }
let(:contentful_model_double) { instance_double(Contentful::Model, promotion_type: 'Promotion 1') }
before(:each) do
# mock return values chain
# note that you are not "expecting" anything yet here
# you're just basically saying that: if Contentful::PartnerCampaign.find_by(vanityUrl: expected_referral_source).load.first is called, that it should return contentful_model_double
allow(Contentful::PartnerCampaign).to receive(:find_by).with(vanityUrl: expected_referral_source) do
double.tap do |find_by_returned_object|
allow(find_by_returned_object).to receive(:load) do
double.tap do |load_returned_object|
allow(load_returned_object).to receive(:first).and_return(contentful_model_double)
end
end
end
end
end
it 'calls Contentful::PartnerCampaign.find_by(vanityUrl: referral_source).load.first' do
expect(Contentful::PartnerCampaign).to receive(:find_by).once do |argument|
expect(argument).to eq({ vanityUrl: expected_referral_source})
double.tap do |find_by_returned_object|
expect(find_by_returned_object).to receive(:load).once do
double.tap do |load_returned_object|
expect(load_returned_object).to receive(:first).once
end
end
end
end
end
it 'does something...' do
# ...
end
it 'does some other thing...' do
# ...
end
If you do not know about ruby's tap method, feel free to check this out
I think you need to refactor the chain in two lines like this:
model = double("Contentful::Model", fields: { promotion_type: "Promotion 1" })
campaign = double
allow(Contentful::PartnerCampaign).to receive(:find_by).with(vanityUrl: 'test_promo_path').and_return(campaign)
allow(campaign).to receive_message_chain(:load, :first).and_return(model)
Then you can write your spec that will pass that attribute to find_by and check the chain.

the right way to change the associated object in rspec

I recently started to test with rspec, so I can strongly be mistaken, correct me if there is a better way
I create two related models
let(:user) {FactoryGirl.create :user}
let!(:participation) {FactoryGirl.create :participation, user: user}
and before one of the tests change one of the related objects
context "when" do
before {participation.prize = 100}
it "" do
binding.pry
end
end
But inside it
participation.prize => 100
user.participatons.select(:prize) => nil
what am I doing wrong ? and how to fix it?
When you say user.participations.select(:prize), you're making a query to the db to get values in the user's participations' prize columns. But when you say before {participation.prize = 100} you're only setting the prize attribute on the participation object. Try saving the participation before the select line:
participation.prize # => 100
participation.save
user.participatons.select(:prize) # => nil
Another possible issue is that user.participations has been memoized by a previous call. Ensure that user.participations.first == participation. If it doesn't, check
1) puts participation.user_id and
2) puts user.participations, user.reload.participations
Lastly, a better way of setting up the test so that you run into this issue less often is something along the lines of:
# let(:price) { 0 } # default price. Optional so that tests won't throw errors if you forget to set it in a context/describe block.
let(:user) {FactoryGirl.create :user}
let!(:participation) {FactoryGirl.create :participation, user: user, price: price}
# ...
context "when ..." do
let(:price) { 100 }
it "" do
binding.pry
end
end
This way, the price is set when you create the model. Following this pattern generally means running into this problem less.

Alternatives to nested conditionals Rails controller

I have a page that presents bits of information in a grid format. What shows up in each grid tile depends on:
Whether the user is logged in or not
What the user clicks on
Other factors
I'm using AJAX to send the controller what the user clicks on and grab fresh content for the tiles.
# Simplified pseudocode example
def get_tile_content
tile_objects = []
if current_user.present?
if params[:user_selected_content] == 'my stuff'
tile_objects = [... some model scope source here...]
elsif params[:user_selected_content] == 'new stuff'
tile_objects = [... some model scope source here...]
elsif params[:user_selected_content] == 'other stuff'
tile_objects = [... some model scope source here...]
end
else
tile_objects = [... some model scope source here...]
end
render :json => tile_objects.to_json || {}
end
Any ideas on how to approach this differently? I tried moving the complexity to models but I found it to be less readable and harder to figure out what is going on.
Looks like a decent case for a case statement (...see what I did there? ;P)
def get_tile_content
tile_objects = []
if current_user.present?
tile_objects = case params[:user_selected_content]
when 'my stuff' then Model.my_stuff
when 'new stuff' then Model.new_stuff
when 'other stuff' then Model.other_stuff
end
else
tile_objects = Model.public_stuff
end
render :json => tile_objects.to_json || {}
end
Sometimes case statements are necessary. Can't really say if that's the situation here since I can't see the larger design of your app, but this at least will clean it up a bit to fewer, easier to read lines, depending on your style preference.
You could wrap the case statement into its own method if you prefer, passing it the value of your parameter.
Another style point is that you don't typically use get_ at the beginning of ruby method names. It's assumed that .blah= is a setter and .blah is a getter, so .get_blah is redundant.

How to test the number of database calls in Rails

I am creating a REST API in rails. I'm using RSpec. I'd like to minimize the number of database calls, so I would like to add an automatic test that verifies the number of database calls being executed as part of a certain action.
Is there a simple way to add that to my test?
What I'm looking for is some way to monitor/record the calls that are being made to the database as a result of a single API call.
If this can't be done with RSpec but can be done with some other testing tool, that's also great.
The easiest thing in Rails 3 is probably to hook into the notifications api.
This subscriber
class SqlCounter< ActiveSupport::LogSubscriber
def self.count= value
Thread.current['query_count'] = value
end
def self.count
Thread.current['query_count'] || 0
end
def self.reset_count
result, self.count = self.count, 0
result
end
def sql(event)
self.class.count += 1
puts "logged #{event.payload[:sql]}"
end
end
SqlCounter.attach_to :active_record
will print every executed sql statement to the console and count them. You could then write specs such as
expect do
# do stuff
end.to change(SqlCounter, :count).by(2)
You'll probably want to filter out some statements, such as ones starting/committing transactions or the ones active record emits to determine the structures of tables.
You may be interested in using explain. But that won't be automatic. You will need to analyse each action manually. But maybe that is a good thing, since the important thing is not the number of db calls, but their nature. For example: Are they using indexes?
Check this:
http://weblog.rubyonrails.org/2011/12/6/what-s-new-in-edge-rails-explain/
Use the db-query-matchers gem.
expect { subject.make_one_query }.to make_database_queries(count: 1)
Fredrick's answer worked great for me, but in my case, I also wanted to know the number of calls for each ActiveRecord class individually. I made some modifications and ended up with this in case it's useful for others.
class SqlCounter< ActiveSupport::LogSubscriber
# Returns the number of database "Loads" for a given ActiveRecord class.
def self.count(clazz)
name = clazz.name + ' Load'
Thread.current['log'] ||= {}
Thread.current['log'][name] || 0
end
# Returns a list of ActiveRecord classes that were counted.
def self.counted_classes
log = Thread.current['log']
loads = log.keys.select {|key| key =~ /Load$/ }
loads.map { |key| Object.const_get(key.split.first) }
end
def self.reset_count
Thread.current['log'] = {}
end
def sql(event)
name = event.payload[:name]
Thread.current['log'] ||= {}
Thread.current['log'][name] ||= 0
Thread.current['log'][name] += 1
end
end
SqlCounter.attach_to :active_record
expect do
# do stuff
end.to change(SqlCounter, :count).by(2)

Passing a simple test

I'm using Rails 3.2's rake tests function. I'm trying to pass a test but it's giving me errors. Btw, when see you how I write, I'm a noob. It's a hacked way of testing, but at least I want to try passing it first.
test "product title must have at least 10 characters" do
ok = %w{ aaaaaaaaaa aaaaaaaaaaa }
bad = %w{ a aa aaa aaaa aaaaa aaaaaa aaaaaaa aaaaaaaa aaaaaaaaa}
ok.each do |name|
assert new_product_title(name).valid?, "#{name} shouldn't be invalid"
end
bad.each do |name|
assert new_product_title(name).invalid?, "#{name} shouldn't be valid"
end
end
with the function
def new_product_title(title)
Product.new(title: title,
description: "yyy",
price: 1,
image_url: "fred.gif")
end
somehow it's not passing.
What's the reason here? And is there a better way to write it?
I'm more concerned about the method. I'm assuming this method is in a product model? It seems what you are trying to do should definitely be controlled by the model, but I don't think you can call a class's method inside the class's definition. I also don't see much utility in a method that creates a new product with specified title, but static description, price, and image_url. If you need default values for specific attributes, you can set those in an initialize method and overwrite them later if needed. Some people frown on setting defaults in initialize so instead you can set them in an after_initialize callback like this:
class Product < ActiveRecord::Base
after_initialize :init
def init
self.description ||= 'yyy'
self.price ||= 1
self.image_url ||= "fred.gif"
end
end
Then whenever you needed to create a new product with a title and the default attributes you can just use
Product.new(:title => "some title")
And if you don't want all the defaults you can just pass the values into new like usual
Product.new(:title => "some other title", :price => 400) # desc & url are still default
About your tests. I always test in RSpec. Since you are using Test Unit (or Mini Test or whatever it is now), my advice my not be correct. But first I would make the variable names more descriptive. Secondly, there are some commas at the end of your assertions that shouldn't be there.
test "product title must have at least 10 characters" do
valid_name = "a" * 10
short_name = "a" * 9
valid_product = Product.new(:name => valid_name)
assert valid_product.valid?
invalid_product = Product.new(:name => short_name)
assert invalid_product.invalid?
end
If you get that working you may want to verify that the product is invalid for the correct reason using an assert equals method on invalid_product.errors.full_messages and the expected string from the error.

Resources