RSpec check if message has been sent on Slack - ruby

I've got a class which is responsible for send a direct message on Slack to every reporter who has not updated his Jira ticket in 2 days. To send message, described class used send_message method (which underneath is HTTParty.post). I'mc using VCR gem but I don't know how to test such behaviour if at the end I'm not getting 2xx or 3xx code.
reporter_reminder_messenger
class ReporterReminderMessenger
def call
fetch_pending.each do |issue|
send_message(issue)
end
end
private
def fetch_pending
#fetch_pending ||= Jira::FetchPendingStatus.new.call
end
def send_message(issue)
MessageSender.new(
user_id: get_user_id(reporter_email(issue)),
message: create_text_message(issue)
).call
end
I was trying to check changes in MessageSender class (fetch_pending.count == 4 from ReporterReminderMessenger.call)
specs
RSpec.describe ReporterReminderMessenger do
let(:reporter_reminder) { ReporterReminderMessenger.new.call }
it 'returns only pending issues' do
VCR.use_cassette('reminder_messenger') do
expect { reporter_reminder }.to change { MessageSender }.by(4)
end
end
end
But I'm getting an error:
NoMethodError:
undefined method `-' for Slack::MessageSender:Class

I believe the issue is that you need to check for MessageSender.count. That said, I would probably not bother using VCR for this and just mock out the requests/responses. Something like:
subject(:reminder) { described_class.new }
let(:call) { reminder.call }
let(:jira_status) { instance_double(Jira::FetchPendingStatus, call: jira_call) }
let(:jira_call) { some_response_hash_or_whatever }
before do
# Ensure any new object created will return the mocked jira_status object
allow(Jira::FetchPendingStatus).to receive(:new) { jira_status }
end
# I'm assuming the intention was to count the number of DB records created?
it { expect { call }.to change { MessageSender.count }.by(4) }
it 'fetches pending JIRA status' do
expect(jira_status).to receive(:call)
call
end
Aside, this code reads more or less like business logic and not really objects that need persisting; you may want to check out CollectiveIdea's Interactor Gem

Related

RSpec Mock - class does not implement the instance method: jql. Perhaps you meant to use `class_double` instead?

I want to mock method which use Jira-Ruby gem with jql lib
def call
client = JIRA::Client.new(options)
client.Issue.jql(
"project = #{project_key} AND
status != Done AND
status != Closed AND
status != Cancelled AND
status != Followup",
query_options
)
end
my mock:
let(:jql_options) do
[
"project = TSW-123 AND
status != Done AND
status != Closed AND
status != Cancelled AND
status != Followup",
query_options
]
end
let(:query_options) do
{
start_at: 0,
max_results: 1000
}
end
let(:jira_client) { instance_double(JIRA::Client) }
let(:issue) { instance_double(JIRA::Resource::Issue) }
let(:issue_factory) { instance_double(JIRA::Resource::Issue) }
before do
allow(JIRA::Client).to receive(:new).with(options).and_return(jira_client)
allow(jira_client).to receive(:Issue).and_return(issue_factory)
allow(issue_factory).to receive(:jql).with(*jql_options).and_return(issue)
end
it 'connect to the project' do
expect(subject.call).to eq(project)
end
I'm getting an error:
JIRA::Resource::Issue class does not implement the instance method: jql. Perhaps you meant to use class_double instead?
A few things:
You're using mocking syntax for static values, not objects. My answer to your other question here should help: https://stackoverflow.com/a/60404227/4024628
IMO, the use of instance_double here is fine because you are mocking instances, though if you are going to call the mocked object's methods you often need to define their responses in the instance_double declaration (as mentioned in my linked comment).
Be careful with testing mocked behavior; you should only need to test that the client object is created with certain options, and that jql is called with specific args (meaning, make sure you're validating your code's behavior against breaking changes -- not the gem's behavior).
Your spec is testing that call returns project, but you haven't defined or mocked that anywhere?
You are also defining issue_factory and issue as instance_doubles of the same class, which is inconsistent with the documentation for the classes you're using (typo?).
Something like this might be sufficient:
# You should always name your subject if you are calling methods on it
subject(:instance) { described_class.new }
let(:client) { instance_double(JIRA::Client)
let(:issue_factory) { instance_double(JIRA::Resource::IssueFactory) }
# This likely needs to be an OpenStruct instead since docu says it returns an obj
let(:project) { instance_double(JIRA::Resource::Project) }
before do
allow(client).to receive(:new).with(options) { client }
allow(client).to receive(:Issue) { issue_factory }
allow(issue_factory).to receive(:jql).with(*jql_options) { project }
end
it 'creates client with correct options' do
expect(client).to receive(:new).with(options)
instance.call
end
it 'calls #jql with correct options' do
expect(issue_factory).to receive(:jql).with(*jql_options)
instance.call
end
it { expect(instance.call).to eq(project) }

Can I spy a class?

In the effort to make more readable my test suite, I'm introducing spies in my specs but I'm not sure how to deal with class methods. Is it possible to "spy a class"?
Let's say I have the following sample code
def publish(post)
Publisher.call(post)
post.save
end
And the correspondent spec
it 'delegates the publishing to Publisher' do
let(:blog) { ... }
let(:post) { ... }
expect(Publisher).to receive(:call).with(post).and_call_original
blog.publish(post)
end
Is it possible to rewrite the spec using a spy?
Thanks
You can use spies on partial doubles via allow plus expect:
it 'delegates the publishing to Publisher' do
let(:blog) { ... }
let(:post) { ... }
allow(Publisher).to receive(:call)
blog.publish(post)
expect(Publisher).to have_received(:call).with(post)
end

Testing and mocking the contents of a block with RSpec

I am writing a unit test for one of my service objects. In this particular case, I needed to use transactions to ensure data integrity. Thus, I have a simple code like so:
class CreateUser
def save
user_klass.db.transaction do
user = user_klass.create(name: name, email: email)
another_model_klass.find_or_create(user_id: user.id, foo: 'foo')
end
end
end
I am using Sequel as my ORM. However, the important point of this question is actually how to test this code. I have been successfully using mocks and stubs but this is the first time I have to stub out something with a block involved.
At first I have a naive spec like so:
describe CreateUser do
describe "#save" do
let(:user) { instance_double("User", id: 1) }
let(:user_klass) { double("Class:User", create: user) }
let(:another_model_klass) { double("Class:AnotherModel") }
let(:name) { 'Test User' }
let(:email) { 'test#test.com' }
let(:foo) { 'foo' }
let(:params) { { name: name, email: email, foo: foo } }
let!(:form) { CreateUser.new(params, user_klass, another_model_klass) }
before do
allow(another_model_klass).to receive(:find_or_create)
end
it "sends create message to the user_klass" do
expect(user_klass).to receive(:create).with({ name: name, email: email}).and_return(user)
form.save
end
it "sends find_or_create message to another_model_klass" do
expect(another_model_klass).to receive(:find_or_create).with(user_id: user.id, foo: foo)
form.save
end
end
end
This gives out an error:
Double "Class:User" received unexpected message :db with (no args)
But if I add the following:
allow(user_klass).to receive_message_chain(:db, :transaction)
It would stub out the contents of the transaction block and it would still fail.
How do set expectations on my spec where:
expect transaction to be used
expect the create message to be sent to user_klass
expect the find_or_create message to another_model_klass
You can do this:
let(:db) { double("DB") }
let(:user_klass) { double("Class:User", create: user, db: db) }
# ...
before do
allow(db).to receive(:transaction).and_yield
# ...
end
That said: you can do that, but I recommend you don't. In fact, I recommend you don't mock the Sequel API at all. I can speak from experience that down the road of mocking APIs you don't own lies brittle, low-value tests and lots of pain. The general approach that I (and many others) recommend is to wrap the API you don't own with your own API that you do own. Then you can integration test your wrapper (without doing mocking or stubbing) and mock or stub your simpler, domain-specific API in all the other places that rely on that functionality.
On a side note, if you're using RSpec 3, I highly recommend you switch your test doubles to verifying doubles as they provide some really nice guarantees that normal doubles don't provide.
Take a look at spies https://github.com/rspec/rspec-mocks#test-spies, you might be able to drop them in your doubles. :-)

Stub (...) received unexpected message (...) with (no args)

I try to write a test using RR. What I need is a stub of a model object.
describe ApplicationController do
subject(:application_controller) { ApplicationController.new }
let(:messages) { ['a1', 'a2', 'a3' ] }
let(:model) { Object.new }
it 'should copy errors to flash' do
stub(model).error_messages { messages }
flash[:error] == nil
subject.copy_errors_to_flash(model)
flash[:error].should == messages
end
end
What I get is
ApplicationController should copy errors to flash
Failure/Error: stub(model).error_messages { messages }
Stub #<Object:0x007ffaa803f930> received unexpected message :error_messages with (no args)
# ./spec/controllers/application_controller_spec.rb:10:in `block (2 levels) in <top (required)>'
I have no idea what am I doing wrong. I think I follow the docs...
You're calling the method 'error_messages on the stub of your model on this line:
stub(model).error_messages { messages }
I presume that you actually want to do something else here, most likely:
model.should_receive(:error_messages).and_return(messages)
which creates a stub method for error_messages and will respond with your messages array whenever your spec tests calls model.error_messages

why each method is invoked?

I found this example code excerpt from Sinatra README webpage
class Stream
def each
100.times { |i| yield "#{i}\n" }
end
end
get('/') { Stream.new }
when I get to the '/' path, it seems invoke the each method on Stream.new object, why is it?
each is called on a stream object, not on the Stream class. To get a stream object, you need to do Stream.new.

Resources