This simple method on a class just run the status method using the safe navigation operator.
def current_status
account&.status
end
But reek report this warning:
MyClass#current_status performs a nil-check [https://github.com/troessner/reek/blob/master/docs/Nil-Check.md]
How can I properly write methods like this to avoid Nil Check?
I've also verified this post from thoughtbot but it seem like "too much" for just a safe navigation operator.
Ruby 2.3.1
The advice from "Example 4" in the linked post is verbose but pretty good :
class MyClass
def initialize(with_account = nil)
#account = Account.new if with_account
end
def current_status
account.status
end
def account
#account || NilAccount.new
end
end
class Account
def status
"Up!"
end
end
class NilAccount
def status
"Down!"
end
end
puts MyClass.new(:with_account).current_status
#=> "Up!"
puts MyClass.new.current_status
#=> "Down!"
If it's "too much" for you, account&.status might be just fine.
Whatever you do : you'll need to test your code as much as possible!
well, tell-dont-ask looks pretty good, but Example 4 looks like an overkill to resolve this specific case.
#andredurao I think, we can use this workaround to pass checks, for some reason reek is fine with it:
def current_status
return unless account
account.status
end
Related
I have a ruby method
def get_status(creds)
client = create_client(creds)
status = client.account_status
client.close_session
status
end
Usually, I optimize this kind of code by tap or yield_self, but here I can't find a nice way to optimize it.
The only solution I have come up:
def get_status(creds)
create_client(creds).yeild_self do |client|
[client, client.account_status]
end.yield_self do |client, status|
client.close_session
status
end
end
But it doesn't better than the original solution, is it?
One could write the following.
class Client
def account_status
"Overdrawn!"
end
def close_session
puts "It's closed"
end
end
def create_client(creds)
Client.new
end
def get_status(creds)
begin
client = create_client(creds)
client.account_status
ensure
client.close_session if client
end
end
get_status("Anything")
It's closed
#=> "Overdrawn!"
Do I prefer this to #1 in the question? No.
Do I prefer this to #2 in the question? Yes!
Do I prefer this to #max's answer? No.
I understand a finalizer could be created using the class method ObjectSpace::define_finalizer.
class Client
def initialize
ObjectSpace.define_finalizer(self, proc { puts "It's finalized!" })
end
def account_status
"Overdrawn!"
end
end
def create_client(creds)
Client.new
end
def get_status(creds)
create_client(creds).account_status
end
get_status("Anything")
#=> "Overdrawn!"
exit
It's finalized!
One must be careful when creating finalizers, as explained Here. I understand a technique sometimes used is to have finalizer's proc reference class-level objects. See, for example, this article, #Amadan's comments below and #Matt's comments on the question. I am not advocating the use of a finalizer. I merely thought readers unfamiliar with finalizers (as I was before writing this) would find this useful.
Let's list the goal of the function:
Open connection
Read value (and return it)
Close connection
I would consider this a "temporary connection", and that leads me to think it could be refactored to a separate method.
Reasoning: The get_status method is concerned with getting the status from a connection - it doesn't have to handle the details of actually closing/opening the connection itself.
def open_temporary_connection(creds, &block)
client = create_client(creds)
result = block.call(client)
client.close_session
result
end
def get_status(creds)
open_temporary_connection(creds, &:account_status)
end
Also, I should mention, I think yield_self is a bit of a trap. Unless you're dead set on making all of your code into a single expression, it makes the code look awkward without offering a lot of benefit.
I like your first version because it is short, easy to read, and easy to understand. I would not change it.
Nevertheless, an alternative version using tap might look like this:
def get_status(creds)
client = create_client(creds)
client.account_status.tap { client.close_session }
end
I was working on coding challenge called Robot name. I also had tests for that. The program passed all the tests. The code is below..
class Robot
attr_accessor :name
##robots = []
def initialize
#name = self.random_name
##robots << self.name
end
def random_name
name = ''
2.times do
name << ('a'..'z').to_a.sample
end
3.times do
name << (1..9).to_a.sample.to_s
end
no_duplicate(name.upcase)
end
def reset
#name = self.random_name
end
def no_duplicate(name)
if ##robots.include? name
reset
else
name
end
end
end
If you need to see the tests file you can look it up here robot_name_tests.
Then I started to refactor and one of the first things was to refactor no_duplicate method. So after refactoring the code looked like this
class Robot
...
# the rest of code stayed the same
def no_duplicate(name)
##robots.include? name ? reset : name
end
end
With this version all tests showed SystemStackError: stack level too deep. Why does it give this error and what is going on behind the scenes in both cases considering the code provided? Thanks!
I like your poetry mode code but it has led you into trouble here.
One way to kinda keep it in poetry mode but fix your operator priority issue is to do this:
def no_duplicate(name)
(##robots.include? name) ? reset : name
end
Update: if you work in Big Corporation With Coding Standards you will need to make it a bit more boring. I thought this was obvious but the gallery is correctly noting the usual solution:
##robots.include?(name) ? reset : name
I have a simple MySQL wrapper class which will run a query and return results.
class Rsql
def initialize(db)
#client = Mysql2::Client
#db = db
end
def execute_query()
client = #client.new(#db)
client.query("select 1")
end
end
I want to test some stuff involving the results of the query, but I don't want to actually connect to a database to get the results. I tried this test, but it doesn't work:
RSpec.describe Rsql do
it "does it" do
mock_database = double
rsql = Rsql.new(mock_database)
mock_mysql_client = double
allow(mock_mysql_client).to receive(:query).and_return({"1" => 1})
allow_any_instance_of(Mysql2::Client).to receive(:new).and_return(mock_mysql_client)
expect(rsql.execute_query).to eq({"1" => 1})
end
end
Replacing allow_any_instance_of() with allow() works. I was under the impression that allow_any_instance_of() was some kind of a global "pretend this class behaves in this way across the entire program" whereas allow() is for specific instances of a class.
Can someone explain this behavior to me? I'm new to Rspec, so I apologize if this answer is blatantly obvious. I tried searching for the answer, but I couldn't come up with the right search string to find one. Maybe I don't know enough to know when I've found it.
As of RSpec 3.3 , any_instance is deprecated and not recommended to use in your tests.
From the docs:
any_instance is the old way to stub or mock any instance of a class
but carries the baggage of a global monkey patch on all classes. Note
that we generally recommend against using this feature.
You should only need to use allow(some_obj) going forward and the documentation has some great examples (see here).
Such as:
RSpec.describe "receive_messages" do
it "configures return values for the provided messages" do
dbl = double("Some Collaborator")
allow(dbl).to receive_messages(:foo => 2, :bar => 3)
expect(dbl.foo).to eq(2)
expect(dbl.bar).to eq(3)
end
end
Edit, if you really want to use any_instance, do so like this:
(Mysql2::Client).allow_any_instance.to receive(:something)
Edit2, your exact stub doesn't work because you're not stubbing an instance, you're stubbing before the object is initialized. In that case you would do allow(Mysql2::Client).to receive(:new).
this Rsql class seems a service
class Rsql
def initialize(db)
#client = Mysql2::Client
#db = db
end
def execute_query()
client = #client.new(#db)
client.query("select 1")
end
end
lets create a test for it, now we should to test this function execute_query with subject ()
and to create clients in db we can use let! like this
let!(:client1) do
FactoryBot.create(...
with this we should not use double or something
require 'rails_helper'
RSpec.describe RsqlTest do
subject(:clients) do
Rsql.execute_query()
end
context 'select' do
let!(:client1) do
FactoryBot.create(...
end
it 'should return records' do
expect(clients).to include(client1)
end
end
end
I am running rspec tests on a catalog object from within a Ruby app, using Rspec::Core::Runner::run:
File.open('/tmp/catalog', 'w') do |out|
YAML.dump(catalog, out)
end
...
unless RSpec::Core::Runner::run(spec_dirs, $stderr, out) == 0
raise Puppet::Error, "Unit tests failed:\n#{out.string}"
end
(The full code can be found at https://github.com/camptocamp/puppet-spec/blob/master/lib/puppet/indirector/catalog/rest_spec.rb)
In order to pass the object I want to test, I dump it as YAML to a file (currently /tmp/catalog) and load it as subject in my tests:
describe 'notrun' do
subject { YAML.load_file('/tmp/catalog') }
it { should contain_package('ppet') }
end
Is there a way I could pass the catalog object as subject to my tests without dumping it to a file?
I am not very clear as to what exactly you are trying to achieve but from my understanding I feel that using a before(:each) hook might be of use to you. You can define variables in this block that are available to all the stories in that scope.
Here is an example:
require "rspec/expectations"
class Thing
def widgets
#widgets ||= []
end
end
describe Thing do
before(:each) do
#thing = Thing.new
end
describe "initialized in before(:each)" do
it "has 0 widgets" do
# #thing is available here
#thing.should have(0).widgets
end
it "can get accept new widgets" do
#thing.widgets << Object.new
end
it "does not share state across examples" do
#thing.should have(0).widgets
end
end
end
You can find more details at:
https://www.relishapp.com/rspec/rspec-core/v/2-2/docs/hooks/before-and-after-hooks#define-before(:each)-block
I am calling an API from a Rails model and I would like to raise an error if the API returns on non-200 code. Otherwise I want to cache/lazy-load the data. This is my method:
def data
#data ||= SNL.get_list(name)
raise StandardError, #data.inspect unless #data.success?
#data
end
This works but I was wondering if I can accomplish this in one line. I tried using the and operator combined with an unless but couldn't get it to work.
Update: I have accepted tokland's answer because I asked for one line and he/she provided two very good solutions. In the end I am actually going to use
def data
#data ||= SNL.get_list(name)
#data.success? ? #data : (raise StandardError, #data.inspect)
end
for readability. I just hated having a third line just to return #data, since an exception will rarely be raised. I feel odiszapc's answer is a the best compromise of brevity and readability. Thanks everyone.
I wouldn't strain to write a one-liner, but you can use tap if you absolutely must:
def data
(#data ||= SNL.get_list(name)).tap { |d| d.success? or raise StandardError.new(d.inspect) }
end
Also with short-circuited logic:
def data
(#data ||= SNL.get_list(name)).success? && #data or
raise StandardError.new(#data.inspect) }
end
You can just use a terneray operator. However, I think its really important to keep your code as readable as possible. And generally in my experience, code that spreads a bit too much horizontally is generally a bit tough to follow.
There is one thing you need to be sure of. If SNL.get_list(name) returns nil and you're trying to use the and operator along with it, it won't work.
This issue has happened with me numerous times. A sample example:
nil and puts 'hello'
try this in your irb. It won't work. This issue has occurred with me numerous times.
Maybe
def data
#data ||= SNL.get_list(name)
#data.success? ? #data : (raise StandardError, #data.inspect)
end
Or I'm not sure, something like:
def data
(#data ||= SNL.get_list(name)).success? ? #data : (raise StandardError, #data.inspect)
end
try
def data
(#data ||= SNL.get_list(name)).success? ? #data : raise(StandardError, #data.inspect)
end
Again #Sohaib's point is valid this is not quite readable! and not a rubbish's way, lot of parentheses