Testing and mocking the contents of a block with RSpec - ruby

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. :-)

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) }

RSpec check if message has been sent on Slack

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

Faker gem generating 2-3 letter strings

The Faker gem generates short, nonsense strings instead of what is described. For example, Faker::Job.title generates "et". If I have a feature test that expects not to find a Faker-generated string on the page, chances are it's going to fail if the string is "et". Surely this is unexpected behaviour, as nobody in the world has the job title "et".
This is my code, the most recent time I checked it the title was as expected, but the role and category were not:
# frozen_string_literal: true
shared_context 'with signatory attributes' do
let(:first_name) { Faker::Name.first_name }
let(:last_name) { Faker::Name.last_name }
let(:email) { Faker::Internet.email }
let(:title) { Faker::Job.title }
let(:mobile) { Faker::Number.number(10) }
let(:employee_num) { Faker::Number.number(10) }
let(:role) { Faker::Job.title }
let(:category) { Faker::Job.title }
end
Looks like Faker isn’t set up to make realistic job titles. But it’s easy to make your own random job titles. I would just sample your own custom array, like this:
let(:title) { %w[Admin Manager Engineer].sample }
You can use regex matcher with word boundaries instead of the short string only, but it is still not bullet-proof.
let(:first_name) { /\b#{Faker::Name.first_name}\b/ }
But maybe it is better to stub the attribute on model itself and raise an Error if it is called.
It seems like it pulls strings from its Lorem Ipsum String Set for some reason. Do you mind sharing your code?

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

how to selectively set a property using DEPENDENCY INJECTION in a grails service for unit testing

EDIT: Please let me be clear, I'm asking how to do this in Grails using Spring Dependency Injection, and NOT Grails' metaclass functionality or new().
I have a grails service that is for analyzing log files. Inside the service I use the current time for lots of things. For unit testing I have several example log files that I parse with this service. These have times in them obviously.
I want my service, DURING UNIT TESTING to think that the current time is no more than a few hours after the last logging statement in my example log files.
So, I'm willing to this:
class MyService {
def currentDate = { -> new Date() }
def doSomeStuff() {
// need to know when is "right now"
Date now = currentDate()
}
}
So, what I want to be able to do is have currentDate injected or set to be some other HARDCODED time, like
currentDate = { -> new Date(1308619647140) }
Is there not a way to do this with some mockWhatever method inside my unit test? This kind of stuff was super easy with Google Guice, but I have no idea how to do it in Spring.
It's pretty frustrating that when I Google "grails dependency injection" all I find are examples of
class SomeController {
// wow look how amazing this is, it's injected automatically!!
// isn't spring incredible OMG!
def myService
}
It feels like all that's showing me is that I don't have to type new ...()
Where do I tell it that when environment equals test, then do this:
currentDate = { -> new Date(1308619647140) }
Am I just stuck setting this property manually in my test??
I would prefer not to have to create a "timeService" because this seems silly considering I just want 1 tiny change.
Groovy is a dynamic language, and as such it allows you to do almost what you're asking for:
class MyServiceTests extends GrailsUnitTestCase {
def testDoSomeStuff() {
def service = new MyService()
service.currentDate = { -> new Date(1308619647140) }
// assert something on service.doSomeStuff()
}
}
Keep in mind this only modifies the service instance, not the class. If you need to modify the class you'll need to work with the metaClass. Take a look at this post by mrhaki.
Another option would be to make the current date a parameter to doSomeStuff(). That way you wouldn't need to modify your service instance.
Thanks for the help guys. The best solution I could come up with for using Spring DI in this case was to do the following in
resources.groovy
These are the two solutions I found:
1: If I want the timeNowService to be swapped for testing purposes everywhere:
import grails.util.GrailsUtil
// Place your Spring DSL code here
beans = {
if (GrailsUtil.environment == 'test') {
println ">>> test env"
timeNowService(TimeNowMockService)
} else {
println ">>> not test env"
timeNowService(TimeNowService)
}
}
2: I could do this if I only want this change to apply to this particular service:
import grails.util.GrailsUtil
// Place your Spring DSL code here
beans = {
if (GrailsUtil.environment == 'test') {
println ">>> test env"
time1(TimeNowMockService)
} else {
println ">>> not test env"
time1(TimeNowService)
}
myService(MyService) {
diTest = 'hello 2'
timeNowService = ref('time1')
}
}
In either case I would use the service by calling
timeNowService.now().
The one strange, and very frustrating thing to me was that I could not do this:
import grails.util.GrailsUtil
// Place your Spring DSL code here
beans = {
if (GrailsUtil.environment == 'test') {
println ">>> test env"
myService(MyService) {
timeNow = { -> new Date(1308486447140) }
}
} else {
println ">>> not test env"
myService(MyService) {
timeNow = { -> new Date() }
}
}
}
In fact, when I tried that I also had a dummy value in there, like dummy = 'hello 2' and then a default value of dummy = 'hello' in the myService class itself. And when I did this 3rd example with the dummy value set in there as well, it silently failed to set, apparently b/c timeNow blew something up in private.
I would be interested to know if anyone could explain why this fails.
Thanks for the help guys and sorry to be impatient...
Since Groovy is dynamic, you could just take away your currentDate() method from your service and replace it by one that suits your need. You can do this at runtime during the setup of your test.
Prior to having an instance of MyService instantiated, have the following code executed:
MyService.metaClass.currentDate << {-> new Date(1308619647140) }
This way, you can have a consistent behavior across all your tests.
However, if you prefer, you can override the instance method by a closure that does the same trick.
Let me know how it goes.
Vincent Giguère

Resources