I'm having trouble getting rspec to properly test an Active Record callback. In my model I have:
after_create :check_twitter_limit, if: Proc.new { |row| row.service == "twitter" }
Where service is the social media site being saved to the db. In essence, if the event is a twitter event, let's check the rate limit.
The method looks like this:
def check_twitter_limit
binding.pry
if EventTracker.within_fifteen_minutes.twitter.size > 12 && EventTracker.within_fifteen_minutes.twitter.size % 3 == 0
alert_notifier.ping("*[#{user}](#{ANALYTICS_URL}#{distinct_id})* (#{organization}) just _#{event}_ and has made *#{EventTracker.within_fifteen_minutes.twitter.size}* twitter requests in the last 15 minutes!")
end
end
Notice the pry binding. I have this in here as proof that the code works. Every time I run rspec, I get into the pry binding, so I know the method is being called.
Here is the spec:
it "should trigger the twitter alert limit when conditions are met" do
expect(EventTracker).to receive(:check_twitter_limit)
EventTracker.create({
event: "Added twitter users to list",
organization: "Org",
user: "Cool Guy",
event_time_stamp: Time.now.to_i - 14400,
distinct_id: "1",
media_link: "http://www.twitter.com",
is_limited: true,
service: "twitter"
})
end
Here is the error:
Failure/Error: expect(EventTracker).to receive(:check_twitter_limit)
(<EventTracker(id: integer, event: string, organization: string, user: string, event_time_stamp: integer, distinct_id: string, media_link: string, is_limited: boolean, service: string, created_at: datetime, updated_at: datetime) (class)>).check_twitter_limit(any args)
expected: 1 time with any arguments
received: 0 times with any arguments
This test fails despite the fact that I know the callback is being triggered when using pry, and I can confirm that the record is being saved to the db.
Any ideas on what is going on?
I think you want an instance of EventTracker to receive check_twitter_limit, not the class itself. You can do this in RSpec 3.0 with
expect_any_instance_of(EventTracker).to receive(:check_twitter_limit)
Related
I have the following code:
## teams_controller.rb
def destroy
ActiveRecord::Base.transaction do
team_admins = team.team_admins
binding.pry
team.destroy!
team_admins.each(&:update_team_admin_role_if_needed!)
end
respond_with_200(team, serializer: V1::User::Management::TeamSerializer)
end
And the corresponding spec to ensure the last line of the above code fires:
## teams_controller_spec.rb
it 'demotes team admins to employees when needed' do
team_admin_account = create(:account)
admin_team_membership = create(:team_membership, team: #team, admin: true, account: team_admin_account)
team_admin_account.update!(role: Role.team_admin)
expect { process_destroy(team_id: #team.slug) }
.to change { team_admin_account.reload.role }
.from(Role.team_admin)
.to(Role.employee)
end
When I use the above code in my application it works as expected, however the spec fails as the account apparently never has their role updated:
expected `team_admin_account.reload.role` to have changed from #<Role id: 4, add_to_first_user_in_organisation: false, title: "Team admin", created_at: "2020-01-03 09:04:28", updated_at: "2020-01-03 09:04:28", management: false, cms_access: false> to #<Role id: 3, add_to_first_user_in_organisation: false, title: "Employee", created_at: "2020-01-03 09:04:28", updated_at: "2020-01-03 09:04:28", management: false, cms_access: false>, but did not change
When I hit the pry point in my spec and quit out straight away, the spec fails. Likewise when there is no pry point.
However when I enter team_admins at the pry point (which returns the one team_admin I create in my spec) and then quit out of the spec, the spec passes and the account has their role updated.
Anyone have any idea why manually calling team_admins makes my spec pass?
Thanks in advance
EDIT:
The following change to the code also makes the spec pass:
def destroy
ActiveRecord::Base.transaction do
team_admins = team.team_admins
puts team_admins ## <---- Adding this makes the spec pass
team.destroy!
team_admins.each(&:update_team_admin_role_if_needed!)
end
respond_with_200(team, serializer: V1::User::Management::TeamSerializer)
end
team_admins is an AssociationRelation from team. This does not execute a query until referenced, such as with each or puts.
team.destroy! is called before team_admins.each is called. So when team_admins.each executes there is no more team and thus no team_admins. You should be able to verify this by watching logs/test.log and looking at the queries and when they are executed.
Congratulations, you've found a bug. Execute team_admins.each before destroying team.
I'm trying to write a test to see if my Hospital model is receiving a custom method look_for_hospitals.
Here is the test:
Rspec.describe HospitalsController, type: :controller do
describe '#search' do
it 'should call the model method to do the search' do
# model should implement method look_for_hospitals
expect(Hospital).to receive(:look_for_hospitals).with('keyword')
# form in search page must be named 'keywords'
get :search, params: {:keywords => 'keyword'}
expect(assigns(:hospitals))
end
end
Here is my model:
class Hospital<ApplicationRecord
def self.look_for_hospitals term
end
end
And here is the method search in the HospitalsController:
def search
keyword = params[:keywords]
#hospitals = Hospital.look_for_hospitals(keyword)
end
When I run my test this is the error I'm having:
1) HospitalsController#search should call the model method to do the search
Failure/Error: expect(Hospital).to receive(:look_for_hospitals).with('keyword')
(Hospital(id: integer, cnes: string, number: integer, address: text, latitude: string, longitude: string, name: string, phones: text, nature: string, specialties: text, rpa: string, microregion: string, created_at: datetime, updated_at: datetime) (class)).look_for_hospitals("keyword")
expected: 1 time with arguments: ("keyword")
received: 0 times
I know there is pretty much nothing implemented yet, but i'm trying the tdd approach of writing the tests first and then the methods.
Sorry if my english is a bit strange, not native english speaker.
Thanks!
As Tom Lord pointed out, the problem was in the before_action line at the begining of the Controller. I just needed to stub those and the problem was gone.
I am currrently working on a project that has to implement dynamic workflow.
Dynamic: I store workflow's states in database table called wf_steps and the workflow gem has to create states for a particular workflow from the database
For that I am trying to use the workflow gem. You can see how it initializes states and corresponding events in the gem's github-page.
My code:
class SpsWorkflow < ActiveRecord::Base
belongs_to :user
has_many :wf_steps
include Workflow
workflow do
# binding.pry
# read wf-states from the database
# for now let event be same for all the states
self.wf_steps.each do |step|
state step.title.to_sym do
event :assign, transitions_to: :assigning
event :hire, transitions_to: :hiring
event :not_hire, transitions_to: :not_hiring
end
end
end
end
Expectation and Problem encountered:
I expected in the code block below the term self.wf_steps would return my SpsWorkflow's instance/collection. However the self keyword returns #<Workflow::Specification:0x000000063e23e8 #meta={}, #states={}> when I use binding.pry inside the workflow method's block ( I commented in the code )
# read wf-states from the database
# for now let event be same for all the states
self.wf_steps.each do |step|
state step.title.to_sym do
Need you help, thanks
EDIT:
I also tried storing the instance in a variable and using the variable inside the block passing to the workflow method call.
class SpsWorkflow < ActiveRecord::Base
include Workflow
sps_instance = self
But I got the instance of the class SpsWorkflow like
SpsWorkflow(id: integer, workflow_state: string, assigned_to: integer, title: string, description: string, organization_id: integer, user_id: integer, created_at: datetime, updated_at: datetime)
but I want
#<SpsWorkflow id: 1, workflow_state: "step1", assigned_to: nil, title: "Software Engineer", description: "Hire best engineer", organization_id: nil, user_id: 1, created_at: "2015-08-08 00:58:12", updated_at: "2015-08-08 00:58:12">
You have used:
workflow do
self.something
end
self in the context of this block will refer to the WorkflowSpecification. If you really want access to the instance of SpsWorkflow, you may have to pass it into the block or assign it to a different variable and use it there.
I finally solved it using a activerecord callback
class SpsWorkflow < ActiveRecord::Base
include Workflow
after_initialize do
sps_instance = self
SpsWorkflow.workflow do
# read wf-states as well as events from the database
sps_instance.wf_steps.each do |step|
state step.title.to_sym do
event :assign, transitions_to: :step2
event :hire, transitions_to: :hire
event :not_hire, transitions_to: :not_hiring
end
end
end
end
belongs_to :user
has_many :wf_steps
end
I am using factory_girl_rails and rspec and run into trouble,
raises the folloing error
1) WebsiteLink when link is external
Failure/Error: website_link.external should be false
expected #<FalseClass:0> => false
got #<WebsiteLink:100584240> => #<WebsiteLink id: nil, website_id: nil, link: nil, external: nil, checked: nil, click_count: nil, transition_count: nil, created_at: nil, updated_at: nil, link_description: nil>
Compared using equal?, which compares object identity,
but expected and actual are not the same object. Use
`expect(actual).to eq(expected)` if you don't care about
object identity in this example.
It is my code in spec.rb file
it "when link is external" do
website = FactoryGirl.create(:website,site_address: "www.socpost.ru")
website_link = FactoryGirl.create(:website_link, link: "www.google.com", website: website)
website_link.external should be true
end
The factory_girl factories
FactoryGirl.define do
factory :website do
sequence(:site_name){ |i| "Facebook#{i}" }
sequence(:site_address){ |i| "www.facebook_#{i}.com" }
sequence(:website_key){ |i| (1234567 + i).to_s }
end
factory :website_link do
sequence(:link){ |i| "www.facebook.com/test_#{i}" }
external false
checked false
click_count 1
transition_count 1
link_description "Hello"
website
end
end
Since I think it's helpful to understand why you received the error you received, here's an explanation:
You're statement had four expressions separated by spaces: website_link.external, should, be and false
Ruby evaluates these from right to left
false is trivial
be is interpreted as a method call with false as an argument
should is interpreted as a method with the result of be as an argument.
should is interpreted relative to subject, since the method wasn't sent to a specific object
Given the error you received, subject was either explicitly set to be WebsiteLink or that was the argument to describe for a parent of the example and thus the implicit subject
website_link.external never got evaluated because the error occurred prior to that point
You forgot to use the dot.
it "when link is external" do
website = FactoryGirl.create(:website,site_address: "www.socpost.ru")
website_link = FactoryGirl.create(:website_link, link: "www.google.com", website: website)
website_link.external.should be true (note the dot between external and should)
end
Give it a try.
So I am trying to ensure that a callback happens on save... My test is:
user = create_user
login user
visit new_post_path
fill_in "post_title", :with => "my post"
user.expects(:publish_post!).once
click_button "post_submit"
and I get:
1) Failure:
test: Post should place a post. (PostTest)
[test/integration/post_test.rb:72:in __bind_1311708168_640179'
/test/test_helper.rb:37:inexpectation_on'
test/integration/post_test.rb:70:in `__bind_1311708168_640179']:
not all expectations were satisfied
unsatisfied expectations:
- expected exactly once, not yet invoked: #.publish_post!(any_parameters)
satisfied expectations:
- allowed any number of times, not yet invoked: Post(id: integer, title: string, method_of_exchange: string, you_tube_url: string, lat: decimal, lng: decimal, bounty: integer, distance: integer, user_id: integer, consider_similar_offers: boolean, description: text, created_at: datetime, updated_at: datetime, address: string, city: string, state: string, zip: string, country: string, county: string, category_id: integer, slug: string, status: string, popularity: integer, delta: boolean, share_count: integer, needs_reply: decimal, needs_offer: decimal, district: string).facets(any_parameters)
- allowed any number of times, not yet invoked: {}.for(any_parameters)
- expected never, not yet invoked: #.publish_post!(any_parameters)
Yet my post model does:
class Post < ActiveRecord::Base
belongs_to :user
after_create :publish
def publish
user.publish_post!
end
end
and my posts controller's create action does indeed assign the user the the post...
class PostsController < ApplicationController
def create
post = Post.new(params[:post])
post.user = current_user
post.save
end
end
...
The functionality works fine when testing manually.. So I don't get why this is not working when automated?
I really wouldn't try to use mocking expectations in an integration test. That kind of testing belongs in low-level unit tests for the Post class itself. Try to think about integration tests as just looking at the system from the outside, as a user would. What effect does the callback have? Can you just check for the outcome of that effect?