RSpec Fails argument match expectation despite no diff - ruby-on-rails

How do you place an expectation that the correct ActiveRecord::Relation is sent as a keyword argument? I've seen this sort of problem before, and in the past using hash_including matcher resolves the issue, but not with ActiveRecord::Relation. It is so frustrating because the error shows that there is no diff between the expectation and the actual received.
I have a spec that looks something like this:
describe ProcessAccountsJob, type: :job do
subject { described_class.new }
let!(:incomplete) { create(:account, :incomplete_account) }
it 'calls process batch service' do
expect(ProcessAccounts).to receive(:batch).with(
accounts: Account.where(id: incomplete.id)
)
subject.perform
end
end
and I get an error that looks like this:
1) ProcessAccounts calls process batch service
Failure/Error: ProcessAccounts.batch(accounts: accounts)
ProcessAccounts received :batch with unexpected arguments
expected: ({:accounts=>#<ActiveRecord::Relation [#<Account id: 14819, account_number: nil...solar: nil, pap: nil, types: [], annualized_usage: nil>]>})
got: ({:accounts=>#<ActiveRecord::Relation [#<Account id: 14819, account_number: nil...solar: nil, pap: nil, types: [], annualized_usage: nil>]>})
Diff:
# ./app/jobs/process_accounts_job.rb:13:in `perform'
# ./spec/jobs/process_accounts_job_spec.rb:9:in `block (2 levels) in <main>'
As mentioned, trying to use hash_including isn't helping. When the spec is changed to:
describe ProcessAccountsJob, type: :job do
subject { described_class.new }
let!(:incomplete) { create(:account, :incomplete_account) }
it 'calls process batch service' do
expect(ProcessAccounts).to receive(:batch).with(
hash_including(accounts: Account.where(id: incomplete.id))
)
subject.perform
end
end
the diff becomes:
-["hash_including(:accounts=>#<ActiveRecord::Relation [#<Account id: 14822, account_number: nil, service_address: \"123 Main St\", created_at: \"2020-07-12 15:50:00\", updated_at: \"2020-07-12 15:50:00\", solar: nil, pap: nil, types: [], annualized_usage: nil>]>)"]
+[{:accounts=>
+ #<ActiveRecord::Relation [#<Account id: 14822, account_number: nil, service_address: "123 Main St", created_at: "2020-07-12 15:50:00", updated_at: "2020-07-12 15:50:00", solar: nil, pap: nil, types: [], annualized_usage: nil>]>}]

It turns out match_array matcher solves the problem in this case; which is pretty misleading because neither the expected nor actual is an array. 🤷🏻‍♂️
describe ProcessAccountsJob, type: :job do
subject { described_class.new }
let!(:incomplete) { create(:account, :incomplete_account) }
it 'calls process batch service' do
expect(ProcessAccounts).to receive(:batch).with(
accounts: match_array(Account.where(id: incomplete.id))
)
subject.perform
end
end

Related

and sometimes I get failed spices because of Time.now, can't understand why

so i have a method in model
class << self
def last_week
start = Time.zone.now.beginning_of_week - 7.days
finish = start + 7.days
where('appointment_at >= ? AND appointment_at < ?', start, finish).order(appointment_at: :desc)
end
end
And I write spec for this method.
RSpec.describe Appointment, type: :model, vcr: { record: :none } do
let!(:time) { Time.now }
let(:appointment_at) { time }
context '.last_week' do
let!(:scoped_appointment) { create(:appointment, appointment_at: time - 2.days) }
let!(:another_appointment) { create(:appointment, appointment_at: time - 16.days) }
it do
travel_to(time) do
expect(Appointment.last_week).to include(scoped_appointment)
expect(Appointment.last_week).not_to include(another_appointment)
end
end
end
end
And sometime i get failed this spec with error.
expected #<ActiveRecord::Relation []> to include #<Appointment id: 18, lead_id: 27, body: nil, appointment_at: "2019-02-25 00:59:47", google_id: nil, ... "pending", user_id: 22, notify: nil, cc_emails: nil, appointment_minutes: nil, status_message: nil>
Diff:
## -1,2 +1,2 ##
-[#<Appointment id: 18, lead_id: 27, body: nil, appointment_at: "2019-02-25 00:59:47", google_id: nil, created_at: "2019-02-27 00:59:47", updated_at: "2019-02-27 00:59:47", timezone: nil, subject: "Meeting with Lead", address: nil, notification: nil, status: "pending", user_id: 22, notify: nil, cc_emails: nil, appointment_minutes: nil, status_message: nil>]
+[]
I can't understand why?
And I have a suggestion that I should tightly set time
in spec_helper.rb
$now = DateTime.parse('2020-01-01 00:00:01 -0500')
will it be right? and why ?
Your test setup is brittle. It will break depending on the day of the week you run your spec.
The scope in your model returns appointments from the previous week, Monday through Sunday (you are calling beginning_of_week and adding 7 days to it)
So if your tests run on a Wednesday, like in the example you provided, the appointment’s appointment_at field will be set to Monday (since you are calculating it as Time.now - 2.days). That means your scope will not cover that appointment.
I suggest you use a specific time in your setup. Given your current setup, using let(:time) { DateTime.parse('2019-02-25 00:00:00') } should work

Rails: why can I send a single record attribute via controller to new record but not array?

Creating articles via the controller in Rails. A simple method, which more or less works; just call the method from some other place and it generates a new article via the back end and fills in the values:
def test_create_briefing
a = Article.new
a.type_id = 27
a.status = 'published'
a.headline = 'This is a headline'
a.lede = 'Our article is about some interesting topic.'
a.body = test_article_text
a.save!
end
If test_article_text is just a single record, this works fine and prints the existing article body into the new article body. Looks right in the view and looks right in "edit". All perfect.
def test_article_text
a = Article.find_by_id(181)
a.body
end
But if I try to do the same thing with the last ten articles, it doesn't work:
def test_article_text
Article.lastten.each do |a|
a.body
end
end
In the view you get:
[#, #, #, #, #, #, #, #, #, #]
And in "edit" you get:
[#<Article id: 357, headline: "This is a headline", lede: "Our article is about some interesting topic.", body: "[#<Article id: 356, headline: \"This is a headline\"...", created_at: "2017-12-31 20:40:16", updated_at: "2017-12-31 20:40:16", type_id: 27, urgency: nil, main: nil, status: "published", caption: nil, source: nil, video: nil, summary: nil, summary_slug: nil, topstory: false, email_to: nil, notification_slug: nil, notification_message: nil, short_lede: nil, short_headline: nil, is_free: nil, briefing_point: nil>, #<Article id: 356, headline: "This is a headline"…etc, etc, etc.
What do I not know? What am I missing?
It is returned as below because the Article.lastten is the returned variable from your controller.
[#<Article id: 357, headline: "This is a headline", lede: "Our article is about some interesting topic.", body: "[#<Article id: 356, headline: \"This is a headline\"...", created_at: "2017-12-31 20:40:16", updated_at: "2017-12-31 20:40:16", type_id: 27, urgency: nil, main: nil, status: "published", caption: nil, source: nil, video: nil, summary: nil, summary_slug: nil, topstory: false, email_to: nil, notification_slug: nil, notification_message: nil, short_lede: nil, short_headline: nil, is_free: nil, briefing_point: nil>, #<Article id: 356, headline: "This is a headline"…etc, etc, etc.
To return all Article body, do as below:
def test_article_text
arr = Array.new
Article.lastten.each do |a|
arr << a.body
end
arr # should be added so it will be the last value returned from your controller
end
So, #Shiko was nearly right, certainly on the right path. Had to manipulate the array a bit and do two things to get it to work:
.join the bits of the array to strip out all of the rubbish;
Concatenate the different bits for each article in a different way than you normally do in a view. So to_sfor each of the attributes, concatenating things "" + "" and rebuilding the url with information available in the array (no link_to, etc.).
The "**"is markdown, because I'm using that, but I suppose you could bung html tags in there if you needed to.
This works:
def test_article_text
arr = Array.new
Article.lastten.each do |a|
arr << "**" + a.headline.to_s + "**: " + a.text.to_s + "[Read now](/articles/#{a.id}-#{a.created_at.strftime("%y%m%d%H%M%S")}-#{a.headline.parameterize})"
end
arr.join("\n\n")
end

Rspec - archives array to match tested array

I'm having this class method on my Post model for getting archives
def self.archives
Post.unscoped.select("YEAR(created_at) AS year, MONTHNAME(created_at) AS month, COUNT(id) AS total")
.group("year, month, MONTH(created_at)")
.order("year DESC, MONTH(created_at) DESC")
end
This is the test I have wrote for my method
context '.archives' do
first = FactoryGirl.create(:post, published_at: Time.zone.now)
second = FactoryGirl.create(:post, published_at: 1.month.ago)
it 'returns articles archived' do
archives = Post.archives()
expect(
[{
year: first.published_at.strftime("%Y"),
month: first.published_at.strftime("%B"),
published: 1
},
{
year: second.published_at.strftime("%Y"),
month: second.published_at.strftime("%B"),
published: 1
}]
).to match_array(archives)
end
end
However I get the following error
expected collection contained: [#<Post id: nil>, #<Post id: nil>]
actual collection contained: [{:year=>"2017", :month=>"October", :published=>1}, {:year=>"2017", :month=>"September", :total=>1}]
the missing elements were: [#<Post id: nil>, #<Post id: nil>]
the extra elements were: [{:year=>"2017", :month=>"October", :total=>1}, {:year=>"2017", :month=>"September", :total=>1}]
So although I have created 2 factories, the archives array is empty. What am I doing wrong?
Rspec standard is to use the let syntax for defining variables within a context or describe block. The test should look something like this:
describe '.archives' do
let!(:first) { FactoryGirl.create(:post, published_at: Time.zone.now) }
let!(:second) { FactoryGirl.create(:post, published_at: 1.month.ago) }
it 'returns year, month, and total for articles archived' do
actual_attributes = Post.archives.map { |post| [post.year, post.month, post.total] }
expected_total = 1 # I don't know why the query is returning 1 for total, but including this for completeness
expected_attributes = [first, second].map { |post| [post.created_at.year, post.created_at.strftime("%B"), expected_total] }
expect(actual_attributes).to match_array(expected_attributes)
end
end
The issue here is that you are comparing records pulled with only a few attributes (the result of your SQL query) with fully-formed records (created by your test). This test pulls the applicable attributes from both groups and compares them.
Actual array is not empty, it's an array of two Post instances with ids unset (because Select in .archives method doesn't contain id field).
You could compare expected hashes not with archives, but with smth like that:
actual = Post.archives().map do |post|
{ year: post["year"].to_s, month: post["month"], published: post["total"] }
end
expected = [{
year: first.published_at.strftime("%Y").to_s,
month: first.published_at.strftime("%B"),
published: 1
},
{
year: second.published_at.strftime("%Y").to_s,
month: second.published_at.strftime("%B"),
published: 1
}]
expect(actual).to match_array(expected)

How can I stub method in rspec model

I have a below method in my model and I want to stub the value of below
"esxi_hosts = RezApi.new.get_esxi(type, vrealm_id)"
with values like [{x: "y"}]
what is the way to do it in rspec.
def create_esxi
if (["vRealm", "Praxis Parent vRealm", "Praxis Child vRealm"].include?(self.collection_type.try(:name)))
esxi_hosts = []
if(( self.parent && self.parent.parent && self.parent.collection_type.name.upcase == "POD" && self.parent.parent.collection_type.name.upcase == "DATACENTER") or ( self.parent && self.parent.parent && self.parent.parent.parent && (self.parent.collection_type.name.upcase == "RELEASE - PRAXIS" or self.parent.collection_type.name.upcase == "RELEASE - SUBSCRIPTION") && self.parent.parent.collection_type.name.upcase == "POD" && self.parent.parent.parent.collection_type.name.upcase == "DATACENTER"))
vrealm_type = collection_type.try(:name)
vrealm_id = "d#{self.parent.parent.instance}p#{self.parent.instance}v#{instance}"
case vrealm_type
when "vRealm"
types = ["vrealm-multitenant-dr2c", "vpc-standard"]
types.each do |type|
if esxi_hosts.empty?
esxi_hosts = RezApi.new.get_esxi(type, vrealm_id)
end
end
when "Praxis Parent vRealm"
esxi_hosts = RezApi.new.get_esxi("praxis-core", vrealm_id)
when "Praxis Child vRealm"
esxi_hosts = RezApi.new.get_esxi("praxis-node-mgmt", vrealm_id)
end
end
if esxi_hosts.flatten.any?
assign_esxi(esxi_hosts)
end
end
end
I have already tried the below code but it didnt work
require 'spec_helper'
describe "Esxi Host creation from Rez Api" do
let(:federation) {create(:federation_collection, parent_id: nil, name: "Test", usage: "Tech Ops hosted in Vmware Data Centers", owner: "--", date_from: "2015-08-26", date_to: nil, collection_type_id: 2, zone_name: "se.vpc.vmw")}
let(:datacenter) {create(:datacenter_collection, name: "Datacenter", parent_id: federation.id)}
let(:pod) {create(:pod_collection, parent_id: datacenter.id)}
let(:fqdn) {"d2p1s0ch10srv0v101-esx0.se.vpc.vmw"}
let(:vrealm1) {create(:vrealm_collection, name: "vRealm1", parent_id: pod.id)}
let(:vrealm2) {create(:vrealm_collection, name: "vRealm2", parent_id: pod.id)}
context "When response has esxi hosts " do
let(:rez_response) {[{"esx0"=>{"nodefqdn"=>"d2p1s0ch10srv0.se.vpc.vmw","fqdn"=>"d2p1s0ch10srv0v101-esx0.se.vpc.vmw","vmk0"=>{"pg_name"=>"d2p1v101-esx-pg-1062","ip_addr"=>"10.141.112.71","netmask"=>"255.255.255.0",},"vmk1"=>{"pg_name"=>"d2p1pod-sto-pg-17","ip_addr"=>"172.16.160.51","netmask"=>"255.255.252.0"},"vmk2"=>{"pg_name"=>"d2p1v101-ftx-pg-1063","ip_addr"=>"172.16.165.126","netmask"=>"255.255.255.0"}}}]}
subject {RezApi.new}
it "should create esxi hosts" do
allow(subject).to receive(:get_esxi).with("type", "101").and_return(rez_response)
expect(subject).to receive(:get_esxi).with("type", "101").and_return(rez_response)
vrealm1.create_esxi
vrealm1.resources.map(&:fqdn).should include(fqdn)
end
end
end
Getting the below error
Failures:
1) Esxi Host creation from Rez Api When response has esxi hosts should create esxi hosts
Failure/Error: expect(subject).to receive(:get_esxi).with("type", "101").and_return(rez_response)
(#<RezApi:0xc26d69c>).get_esxi("type", "101")
expected: 1 time with arguments: ("type", "101")
received: 0 times
subject { RezApi.new }
context "....." do
it "....." do
allow(subject).to receive(:get_esxi).with("type", "101").and_return({x: "y"})
expect(subject).to receive(:get_esxi).with("type", "101").and_return({x: "y"})
end
end
So, your updated test code should be this:
describe "Esxi Host creation from Rez Api" do
let(:federation) {create(:federation_collection, parent_id: nil, name: "Test", usage: "Tech Ops hosted in Vmware Data Centers", owner: "--", date_from: "2015-08-26", date_to: nil, collection_type_id: 2, zone_name: "se.vpc.vmw")}
let(:datacenter) {create(:datacenter_collection, name: "Datacenter", parent_id: federation.id)}
let(:pod) {create(:pod_collection, parent_id: datacenter.id)}
let(:fqdn) {"d2p1s0ch10srv0v101-esx0.se.vpc.vmw"}
let(:vrealm1) {create(:vrealm_collection, name: "vRealm1", parent_id: pod.id)}
let(:vrealm2) {create(:vrealm_collection, name: "vRealm2", parent_id: pod.id)}
context "When response has esxi hosts " do
let(:rez_response) {[{"esx0"=>{"nodefqdn"=>"d2p1s0ch10srv0.se.vpc.vmw","fqdn"=>"d2p1s0ch10srv0v101-esx0.se.vpc.vmw","vmk0"=>{"pg_name"=>"d2p1v101-esx-pg-1062","ip_addr"=>"10.141.112.71","netmask"=>"255.255.255.0",},"vmk1"=>{"pg_name"=>"d2p1pod-sto-pg-17","ip_addr"=>"172.16.160.51","netmask"=>"255.255.252.0"},"vmk2"=>{"pg_name"=>"d2p1v101-ftx-pg-1063","ip_addr"=>"172.16.165.126","netmask"=>"255.255.255.0"}}}}
subject { RezApi.new }
it "should create esxi hosts" do
allow(subject).to receive(:get_esxi).with("vrealm-multitenant-dr2c", "101").and_return(rez_response)
expect(subject).to receive(:get_esxi).with("vrealm-multitenant-dr2c", "101").and_return(rez_response)
vrealm1.create_esxi
vrealm1.resources.map(&:fqdn).should include(fqdn)
end
end
end
It worked with below code
RezApi.any_instance.stub(:get_esxi).with("vrealm-multitenant-dr2c", "101").and_return(success_response)

Access attributes in ActiveRecord::Associations::CollectionProxy

I am trying to write a custom function that will throw an error if the amount of associated objects are >=4
I am wondering how i can access the keys/values in the contained hash and run a validation on it
if i do this
animal = FactoryGirl.create(:animal, images_count: 4)
a = animal.animal_images
ap(a)
I get this returned
#<ActiveRecord::Associations::CollectionProxy [
#<AnimalImage id: 520, animal_id: 158, image: "yp2.jpg", created_at: "2014-10-15 13:45:11", updated_at: "2014-10-15 13:45:11">,
#<AnimalImage id: 521, animal_id: 158, image: "yp2.jpg", created_at: "2014-10-15 13:45:11", updated_at: "2014-10-15 13:45:11">,
#<AnimalImage id: 522, animal_id: 158, image: "yp2.jpg", created_at: "2014-10-15 13:45:11", updated_at: "2014-10-15 13:45:11">,
#<AnimalImage id: 523, animal_id: 158, image: "yp2.jpg", created_at: "2014-10-15 13:45:11", updated_at: "2014-10-15 13:45:11">
]
So i thought of using .map
animal = FactoryGirl.create(:animal, images_count: 4)
a = animal.animal_images
map = a.each.map { |i| i.image }
if map.length >= 4
ap('MORE THAN 4 IMAGES')
end
"MORE THAN 4 IMAGES"
So that iterates through the CollectionProxy. However how can i get this formatted into a correct rspec test and perform the logic in a custom validation function.
I thought my test would look like this
it 'should display an error message when too many images are uploaded' do
animal = FactoryGirl.create(:animal, images_count: 4)
animal.max_num_of_images
expect(animal.errors[:base]).to include("Max of 3 images allowed")
end
and just to get the pass for now (add error message) with no logic i have
class AnimalImage < ActiveRecord::Base
belongs_to :animal
validate :max_num_of_images, :if => "image?"
def max_num_of_images
errors.add(:base, "Max of 3 images allowed")
end
end
but it seems as if the test doesnt get past the first line
ActiveRecord::RecordInvalid:
Validation failed: Max of 3 images allowed
the above is thrown in the console
This is my factory
FactoryGirl.define do
factory :animal, class: Animal do
ignore do
images_count 0
end
after(:create) do |animal, evaluator|
create_list(:animal_image, evaluator.images_count, animal: animal)
end
end
end
FactoryGirl.define do
factory :animal_image do
image { File.open("#{Rails.root}/spec/fixtures/yp2.jpg") }
end
end
i'm probably going about this in the most backwards way possible, does anyone have any suggestions please
thanks
I am trying to write a custom function that will throw an error if the amount of associated objects are >=4
You are overcomplicating things. If you just want to count the number of records in a collection then you can simply do animal.animal_images.size. So your model will look like this:
class Animal < ActiveRecord::Base
has_many :animal_images
validate :max_num_of_images
def max_num_of_images
errors.add(:base, "Max of 3 images allowed") if self.animal_images.size >= 4
end
end

Resources