I'm using Ruby 1.9.2. For example i've got class :
class Test
def ==(param)
# some process
end
def bar(param)
puts "foo bar #{param}"
end
end
I can invoke bar method using :
Test.new.instance_eval{ bar 'celona' }
But i cannot execute == method from block like
Test.new.instance_eval{ == "foo" }
i've got syntax error, unexpected tEQ
The following worked for me:
class Test
def ==(param)
p "You put #{param}"
end
end
Test.new.instance_eval{|a| a == "foo" }
=> "You put foo"
The solution really depends on your use case.
Edit The same holds true for when you instantiate a class.
b = Test.new
b == "foo"
=> "You put foo"
You can also use self
Test.new.instance_eval{self == "foo" }
I am not entirely sure of the reason, but I would guess that the == method required an explicit callee and it can't infer self
You can try :
Test.new.send("==", "foo")
Related
class ExternalObject
attr_accessor :external_object_attribute
def update_external_attribute(options = {})
self.external_object_attribute = [1,nil].sample
end
end
class A
attr_reader :my_attr, :external_obj
def initialize(external_obj)
#external_obj = external_obj
end
def main_method(options = {})
case options[:key]
when :my_key
self.my_private_method(:my_key) do
external_obj.update_external_attribute(reevaluate: true)
end
else
nil
end
end
private
def my_private_method(key)
old_value = key
external_object.external_object_attribute = nil
yield
external_object.external_object_attribute = old_value if external_object.external_object_attribute.nil?
end
end
I want to test following for main_method when options[:key] == :my_key:
my_private_method is called once with argument :my_key and it has a block {external_obj.update_external_attribute(reevaluate: true) } , which calls update_external_attribute on external_obj with argument reevaluate: true once.
I'm able to test my_private_method call with :my_key argument once.
expect(subject).to receive(:my_private_method).with(:my_key).once
But how do I test the remaining part of the expectation?
Thank you
It could be easier to answer your question if you post your test as well.
The setup, the execution and asseriotns/expectations.
You can find a short answer in this older question.
You can find useful to read about yield matchers.
I would suggest to mock the ExternalObject if you already haven't. But I can't tell unless you post your actual test code.
I'm going to answer your question. But, then I'm going to explain why you should not do it that way, and show you a better way.
In your test setup, you need to allow the double to yield so that the code will fall through to your block.
RSpec.describe A do
subject(:a) { described_class.new(external_obj) }
let(:external_obj) { instance_double(ExternalObject) }
describe '#main_method' do
subject(:main_method) { a.main_method(options) }
let(:options) { { key: :my_key } }
before do
allow(a).to receive(:my_private_method).and_yield
allow(external_obj).to receive(:update_external_attribute)
main_method
end
it 'does something useful' do
expect(a)
.to have_received(:my_private_method)
.with(:my_key)
.once
expect(external_obj)
.to have_received(:update_external_attribute)
.with(reevaluate: true)
.once
end
end
end
That works. The test passes. RSpec is a powerful tool. And, it will let you get away with that. But, that doesn't mean you should. Testing a private method is ALWAYS a bad idea.
Tests should only test the public interface of a class. Otherwise, you'll lock yourself into the current implementation causing the test to fail when you refactor the internal workings of the class - even if you have not changed the externally visible behavior of the object.
Here's a better approach:
RSpec.describe A do
subject(:a) { described_class.new(external_obj) }
let(:external_obj) { instance_double(ExternalObject) }
describe '#main_method' do
subject(:main_method) { a.main_method(options) }
let(:options) { { key: :my_key } }
before do
allow(external_obj).to receive(:update_external_attribute)
allow(external_obj).to receive(:external_object_attribute=)
allow(external_obj).to receive(:external_object_attribute)
main_method
end
it 'updates external attribute' do
expect(external_obj)
.to have_received(:update_external_attribute)
.with(reevaluate: true)
.once
end
end
end
Note that the expectation about the private method is gone. Now, the test is only relying on the public interface of class A and class ExternalObject.
Hope that helps.
I am a complete beginner to Ruby. I am working on Lesson 45 of Learn Ruby the Hard Way currently and am creating a game similar to Zork and Adventure.
I have created a structure where I am creating 'scenes' in different files and requiring all the scenes in one file where I have an engine/map that ensures if the current scene does not equal 'finished' that it runs 'X' scene's 'enter' method.
However I have two issues:
1) I keep getting a error saying 'Warning class variable access from top level'
2) Even though the script is running I get
ex45.rb:30:in `play': undefined method `enter' for nil:NilClass (NoMethodError) from ex45.rb:59:in
The following is all of my code from each file. My apologies if it's a long read, but I would love to know why I am getting these two errors and what I can do to fix them.
Ex45.rb:
require "./scene_one.rb"
require "./scene_two.rb"
require "./scene_three.rb"
##action = SceneOne.new
##action_two = SceneTwo.new
##action_three = SceneThree.new
class Engine
def initialize(scene_map)
#scene_map = scene_map
end
def play()
current_scene = #scene_map.opening_scene()
last_scene = #scene_map.next_scene('finished')
while current_scene != last_scene
next_scene_name = current_scene.enter()
current_scene = #scene_map.next_scene(next_scene_name)
end
current_scene.enter()
end
end
class Map
##scenes = {
'scene_one' => ##action,
'scene_two' => ##action_two,
'scene_three' => ##action_three
}
def initialize(start_scene)
#start_scene = start_scene
end
def next_scene(scene_name)
val = ##scenes[scene_name]
return val
end
def opening_scene()
return next_scene(#start_scene)
end
end
a_map = Map.new('scene_one')
a_game = Engine.new(a_map)
a_game.play()
scene_one.rb:
class SceneOne
def enter
puts "What is 1 + 2?"
print "> "
answer = $stdin.gets.chomp
if answer == "3"
puts "Good job"
return 'scene_two'
else
puts "try again"
test
end
end
end
scene_two.rb
class SceneTwo
def enter
puts "1 + 3?"
print "> "
action = $stdin.gets.chomp
if action == "4"
return 'scene_three'
else
puts "CANNOT COMPUTE"
end
end
end
scene_three.rb
class SceneThree
def enter
puts "This is scene three"
end
end
Thanks in advance!
Answer to your first question:
You need to move the class variable definitions inside your Map class to get rid of these warnings:
Ex45.rb:5: warning: class variable access from toplevel
Ex45.rb:6: warning: class variable access from toplevel
Ex45.rb:7: warning: class variable access from toplevel
So, your Map class would look like this:
class Map
##action = SceneOne.new
##action_two = SceneTwo.new
##action_three = SceneThree.new
##scenes = {
'scene_one' => ##action,
'scene_two' => ##action_two,
'scene_three' => ##action_three
}
def initialize(start_scene)
#start_scene = start_scene
end
def next_scene(scene_name)
val = ##scenes[scene_name]
return val
end
def opening_scene()
return next_scene(#start_scene)
end
end
To answer your 2nd question:
You are getting undefined method 'enter' for nil:NilClass (NoMethodError) because your current_scene becomes nil at some point and then you try to call: current_scene.enter() i.e. nil.enter and it fails with that error message.
To solve this problem, you have to make sure you always have some value in your current_scene i.e. make sure it's not nil.
I think, you can just remove current_scene.enter() line from the end of your play method in the Engine class. So, your Engine class will look like this:
class Engine
def initialize(scene_map)
#scene_map = scene_map
end
def play()
current_scene = #scene_map.opening_scene()
last_scene = #scene_map.next_scene('finished')
while current_scene != last_scene
next_scene_name = current_scene.enter()
current_scene = #scene_map.next_scene(next_scene_name)
end
# current_scene.enter()
end
end
And, you won't get that error anymore.
Just so you know:
##y = 20
p Object.class_variables
--output:--
1.rb:1: warning: class variable access from toplevel
[:##y]
And:
class Object
def self.y
##y
end
end
puts Object.y
--output:--
20
But:
class Dog
##y = "hello"
def self.y
##y
end
end
puts Dog.y #=>hello
puts Object.y #=>What do you think?
The output of the last line is the reason that class variables are not used in ruby. Instead of class variables, you should use what are known as class instance variables:
class Object
#y = 10 #class instance variable
def self.y
#y
end
end
puts Object.y
class Dog
#y = "hello"
def self.y
#y
end
end
puts Dog.y #=> hello
puts Object.y #=> 10
A class instance variable is just an #variable that is inside the class, but outside any def. And instead of there being one ##variable that is shared by all the subclasses, each subclass will have its own #variable.
I had a code looking like this:
def my_function(obj)
if obj.type == 'a'
return [:something]
elsif obj.type == 'b'
return []
elsif obj.type == 'c'
return [obj]
elsif obj.type == 'd'
return [obj]*2
end
end
I want to separate all these if...elsif blocks into functions like this:
def my_function_with_a
return [:something]
end
def my_function_with_b
return []
end
def my_function_with_c(a_parameter)
return [a_parameter]
end
def my_function_with_d(a_parameter)
return [a_parameter] * 2
end
I call these functions with
def my_function(obj)
send(:"my_function_with_#{obj.type}", obj)
end
The problem is that some functions need parameters, others do not. I can easily define def my_function_with_a(nothing=nil), but I'm sure there is a better solution to do this.
#Dogbert had a great idea with arity. I have a solution like this:
def my_function(obj)
my_method = self.method("my_function_with_#{obj.type}")
return (method.arity.zero? ? method.call : method.call(obj))
end
Check how to call methods in Ruby, for that I will recommend you this two resources: wikibooks and enter link description here.
Take a special note on optional arguments where you can define a method like this:
def method(*args)
end
and then you call call it like this:
method
method(arg1)
method(arg1, arg2)
def foo(*args)
[ 'foo' ].push(*args)
end
>> foo
=> [ 'foo' ]
>> foo('bar')
=> [ 'foo', 'bar' ]
>> foo('bar', 'baz')
=> [ 'foo', 'bar', 'baz' ]
def my_function(obj)
method = method("my_function_with_#{obj.type}")
method.call(*[obj].first(method.arity))
end
Change your function to something like:
def my_function_with_foo(bar=nil)
if bar
return ['foo', bar]
else
return ['foo']
end
end
Now the following will both work:
send(:"my_function_with_#{foo_bar}")
=> ['foo']
send(:"my_function_with_#{foo_bar}", "bar")
=> ['foo', 'bar']
You can also write it like this if you don't want to use if/else and you're sure you'll never need nil in the array:
def my_function_with_foo(bar=nil)
return ['foo', bar].compact
end
You can use a default value
def fun(a_param = nil)
if a_param
return ['raboof',a_param]
else
return ['raboof']
end
end
or...
def fun(a_param : nil)
if a_param
return ['raboof',a_param]
else
return ['raboof']
end
end
The latter is useful if you have multiple parameters because now when you call it you can just pass in the ones that matter right now.
fun(a_param:"Hooray")
I'm making an task-manager and have an boolean attribute for 'finished'. I've tried to override the setter to implement an 'finished_at' date when i toggle 'finished' to true.
But i getting some mixed result. It doesn't work in browser but it will work in my rspec test.
Please help me out.
class TasksController < ApplicationController
# ...
def update
# ..
if #task.update_attributes(params[:task]) # where params[:task][:finished] is true
# ...
end
class Task < ActiveRecord::Base
#...
def finished=(f)
write_attribute :finished, f
write_attribute :finished_at, f == true ? DateTime.now : nil
end
end
# and in rspec i have
describe "when marked as finished" do
before { #task.update_attributes(finished: true) }
its(:finished_at) { should_not be_nil }
its(:finished_at) { should > (DateTime.now - 1.minute) }
describe "and then marked as unfinished" do
before { #task.update_attributes(finished: false) }
its(:finished_at) { should be_nil }
end
end
in browser it executes "UPDATE "tasks" SET "finished" = 't', "updated_at" = '2012-10-02 18:55:07.220361' WHERE "tasks"."id" = 17"
and in rails console i got the same with update_attributes.
But in rspec with update_attributes i get "UPDATE "tasks" SET "finished" = 't', "finished_at" = '2012-10-02 18:36:47.725813', "updated_at" = '2012-10-02 18:36:51.607143' WHERE "tasks"."id" = 1"
So I use the same method but it's only working in rspec for some reson...
using latest rails and latest spec (not any rc or beta).
Solution
Not mush i did need to edit. Thanks #Frederick Cheung for the hint.
I did notice i did like "self[:attr]" more than "write_attribute". Looks better imo.
def finished=(value)
self[:finished] = value
self[:finished_at] = (self.finished? ? Time.now.utc : nil)
end
Your setter is passed the values as they are passed to update_attributes. In particular when this is triggered by a form submission (and assuming you are using the regular rails form helpers) f will actually be "0" or "1", so the comparison with true will always be false.
The easiest thing would be to check the value of finished? after the first call to write_attribute, so that rails can convert the submitted value to true/false. It's also unrubyish to do == true - this will break if the thing you are testing returns a truthy value rather than actually true (for example =~ on strings returns an integer when there is a match)
You could use ActiveRecord Dirty Tracking to be notified of this change.
http://api.rubyonrails.org/classes/ActiveModel/Dirty.html
class Task < ActiveRecord::Base
before_save :toggle_finished_at
def toggle_finished_at
if finished_changed?
before = changes['finished'][0]
after = changes['finished'][1]
# transition from finished => not-finished
if before == true && after == false
self.finished_at = nil
end
# transition from not finished => finished
if before == false && after == true
self.finished_at = Time.now.utc
end
end
end
end
This is a use case for a state machine. You call a :finish! event (a method) which is configured to change the state and to do whatever else needed.
https://github.com/pluginaweek/state_machine/
Let's assume that i have this class
class Foo
def bar(param1=nil, param2=nil, param3=nil)
:bar1 if param1
:bar2 if param2
:bar3 if param3
end
end
I can stub whole bar method using:
Foo.any_instance.expects(:bar).at_least_once.returns(false)
However if I only want to stub when param1 of bar method is true, I couldn't find a way to do:
I also looked at with, and has_entry, and it seems it only applies to a single parameter.
I was expecting some function like this.
Foo.any_instance.expects(:bar).with('true',:any,:any).returns(:baz1)
Foo.any_instance.expects(:bar).with(any,'some',:any).returns(:baz2)
Thanks
................................................... EDITED THE FOLLOWING .............................................
Thanks, nash
Not familiar with rspec, so I tried with unit test with any_instance, and it seems work
require 'test/unit'
require 'mocha'
class FooTest < Test::Unit::TestCase
def test_bar_stub
foo = Foo.new
p foo.bar(1)
Foo.any_instance.stubs(:bar).with { |*args| args[0]=='hee' }.returns('hee')
Foo.any_instance.stubs(:bar).with { |*args| args[1]=='haa' }.returns('haa')
Foo.any_instance.stubs(:bar).with { |*args| args[2]!=nil }.returns('heehaa')
foo = Foo.new
p foo.bar('hee')
p foo.bar('sth', 'haa')
p foo.bar('sth', 'haa', 'sth')
end
end
If I got you right it can be something like:
class Foo
def bar(param1=nil, param2=nil, param3=nil)
:bar1 if param1
:bar2 if param2
:bar3 if param3
end
end
describe Foo do
it "returns 0 for all gutter game" do
foo = Foo.new
foo.stub(:bar).with { |*args| args[0] }.and_return(:bar1)
foo.stub(:bar).with { |*args| args[1] }.and_return(:bar2)
foo.stub(:bar).with { |*args| args[2] }.and_return(:bar3)
foo.bar(true).should == :bar1
foo.bar('blah', true).should == :bar2
foo.bar('blah', 'blah', true).should == :bar3
end
end