RSpec loop testing with console input and output - ruby

I have an input method, that intended to read circle radius from console. If input is invalid, method outputs error message and loops to read input again.
So I need to make an rspec test that iterates by array of invalid inputs and expect that
input method will output error to console message each time.
Here is my input class:
# frozen_string_literal: true
require_relative '../data/messages'
# Input class is responsible for reading and writing to and from console and
# querying user
class Input
def read
loop do
print "#{RADIUS_QUERY_MSG}\n> "
radius = gets.strip
return radius.to_f if valid?(radius)
puts INVALID_MSG
end
end
private
def valid?(radius)
/\A[+]?\d+(\.\d+)?\z/.match(radius)
end
end
I've tried this in my rspec test, but it seems to get into some infinite loop:
# frozen_string_literal: true
require 'input'
require_relative '../data/messages'
require_relative '../data/rspec'
RSpec.describe Input do
let(:input) { described_class.new }
describe '#read' do
INVALID_INPUTS.each do |invalid_input|
context "with invalid input \"#{invalid_input}\"" do
it 'tells user that input is invalid' do
allow(input).to receive(:gets).and_return(invalid_input)
expect(input.read).to output("#{INVALID_MSG}\n").to_stdout
end
end
end
end
end
How can I do this properly? Would appreciate any help.
P.S.
Found this article, but it was no use for me. Maybe it will help. https://haughtcodeworks.com/blog/software-development/easy-loop-testing/
P.P.S.
INVALID_MSG and RADIUS_QUERY_MSG are strings and INVALID_INPUTS is an array of strings.

Refactor to Inject Your Test Inputs into the "Real" Method
This is a common problem for code that isn't written test-first. There are a couple of ways to solve it, but the simplest option without mocking, stubbing, or otherwise invalidating your "real" code is simply to refactor the method itself. For example:
def read test_input: nil
loop do
print "#{RADIUS_QUERY_MSG}\n> "
radius = (test_input || gets).strip
return radius.to_f if valid?(radius)
puts INVALID_MSG
end
end
Now you can simply inject whatever values you want into the optional test_input keyword argument from your RSpec tests to ensure that the input is stripped properly and exhibits whatever other behavior you're testing for.
This avoids all sorts of problems you might experience by trying to write around a difficult-to-test method. Either you provide test input directly to the method, in which case the method uses that, or you don't, in which case it calls #gets just as it normally would.
Remember, the goal isn't to test core methods like #gets. Instead, you should be testing the behavior of your method or object given a particular input or state. If you make your methods testable by allowing dependency injection in your code, or refactoring your class to allow modifying instance variables in your test setup and using those rather than method arguments passed to your methods, you ensure that you are testing your real class or method rather than hacking your way around it.
There are certainly other, more complex ways to do what I did above, but they don't seem warranted for this specific example. The KISS principle definitely applies!

Related

A better way to call methods on an instance

My question has a couple layers to it so please bear with me? I built a module that adds workflows from the Workflow gem to an instance, when you call a method on that instance. It has to be able to receive the description as a Hash or some basic data structure and then turn that into something that puts the described workflow onto the class, at run-time. So everything has to happen at run-time. It's a bit complex to explain what all the crazy requirements are for but it's still a good question, I hope. Anyways, The best I can do to be brief for a context, here, is this:
Build a class and include this module I built.
Create an instance of Your class.
Call the inject_workflow(some_workflow_description) method on the instance. It all must be dynamic.
The tricky part for me is that when I use public_send() or eval() or exec(), I still have to send some nested method calls and it seems like they use 2 different scopes, the class' and Workflow's (the gem). When someone uses the Workflow gem, they hand write these method calls in their class so it scopes everything correctly. The gem gets to have access to the class it creates methods on. The way I'm trying to do it, the user doesn't hand write the methods on the class, they get added to the class via the method shown here. So I wasn't able to get it to work using blocks because I have to do nested block calls e.g.
workflow() do # first method call
# first nested method call. can't access my scope from here
state(:state_name) do
# second nested method call. can't access my scope
event(:event_name, transitions_to: :transition_to_state)
end
end
One of the things I'm trying to do is call the Workflow#state() method n number of times, while nesting the Workflow#event(with, custom_params) 0..n times. The problem for me seems to be that I can't get the right scope when I nest the methods like that.
It works just like I'd like it to (I think...) but I'm not too sure I hit the best implementation. In fact, I think I'll probably get some strong words for what I've done. I tried using public_send() and every other thing I could find to avoid using class_eval() to no avail.
Whenever I attempted to use one of the "better" methods, I couldn't quite get the scope right and sometimes, I was invoking methods on the wrong object, altogether. So I think this is where I need the help, yeah?
This is what a few of the attempts were going for but this is more pseudo-code because I could never get this version or any like it to fly.
# Call this as soon as you can, after .new()
def inject_workflow(description)
public_send :workflow do
description[:workflow][:states].each do |state|
state.map do |name, event|
public_send name.to_sym do # nested call occurs in Workflow gem
# nested call occurs in Workflow gem
public_send :event, event[:name], transitions_to: event[:transitions_to]
end
end
end
end
end
From what I was trying, all these kinds of attempts ended up in the same result, which was my scope isn't what I need because I'm evaluating code in the Workflow gem, not in the module or user's class.
Anyways, here's my implementation. I would really appreciate it if someone could point me in the right direction!
module WorkflowFactory
# ...
def inject_workflow(description)
# Build up an array of strings that will be used to create exactly what
# you would hand-write in your class, if you wanted to use the gem.
description_string_builder = ['include Workflow', 'workflow do']
description[:workflow][:states].each do |state|
state.map do |name, state_description|
if state_description.nil? # if this is a final state...
description_string_builder << "state :#{name}"
else # because it is not a final state, add event information too.
description_string_builder.concat([
"state :#{name} do",
"event :#{state_description[:event]}, transitions_to: :#{state_description[:transitions_to]}",
"end"
])
end
end
end
description_string_builder << "end\n"
begin
# Use class_eval to run that workflow specification by
# passing it off to the workflow gem, just like you would when you use
# the gem normally. I'm pretty sure this is where everyone's head pops...
self.class.class_eval(description_string_builder.join("\n"))
define_singleton_method(:has_workflow?) { true }
rescue Exception => e
define_singleton_method(:has_workflow?) { !!(puts e.backtrace) }
end
end
end
end
# This is the class in question.
class Job
include WorkflowFactory
# ... some interesting code for your class goes here
def next!
current_state.events.#somehow choose the correct event
end
end
# and in some other place where you want your "job" to be able to use a workflow, you have something like this...
job = Job.new
job.done?
# => false
until job.done? do job.next! end
# progresses through the workflow and manages its own state awareness
I started this question off under 300000 lines of text, I swear. Thanks for hanging in there! Here's even more documentation, if you're not asleep yet.
module in my gem

RSpec testing of a class which uses a gem object as an instance variable

So I'm pretty new to Rspec and I'm trying to figure out how to write tests for a class that takes an object as a constructor parameter and sets that object to an instance variable. Then it calls that instance variable's object methods in other methods.
Example:
class ClassA
def initialize(string_object, gem_object)
#instance_variable1 = gem_object
#string = string_object
end
def check_validity?(some_arg)
unless #instance_variable1.gemObjectMethod1.gemObjectMethod2(some_arg).empty?
return true
end
false
end
..
..
end
I feel very lost in how to write specifications for this. For one I don't really understand what specifying a constructor actually entails. What I realize is that I'd have to find some way of mocking or stubbing the gem_object I'm getting as argument, but I'm not sure how.
For the next method, what I've tried to this point is:
describe '#check_validity?' do
context 'gets empty list' do
let (:actual) { subject.check_validity?("sample") }
before do
allow(subject).to receive(#instance_variable1.gemObjectMethod1.gemObjectMethod2).with("sample").and_return([])
end
it 'returns false' do
expect(actual).to be false
end
end
end
But this gives me error relating to my constructor saying that it expected 2 arguments but was given 0.
Any help would be much appreciated! Also, I couldn't really find anything on line about specifying constructors with their arguments mocked. Maybe I'm looking in the wrong place or maybe missing something obvious as this is my first experience with BDD.
In RSpec, 'receive' is a method that accepts a symbol that represents the name of a method. (It allows you to chain a 'with' method that accepts the expected list of parameters.) To fix the before-block you could do this:
before do
allow(subject.instance_variable_get(:#instance_variable1).gemObjectMethod1).to receive(:gemObjectMethod2).with("sample").and_return([])
end
The sheer ugliness of that, though, suggests that something is wrong. And it is. The code is violating the law of demeter pretty badly and the test is being drawn into it.
As a first attempt to clean it up, you might consider a method that "caches" the results of calling #instance_variable1.gemObjectMethod1. Let's say that that first method returns an enumerable group of widgets. You could change your class to include something like this:
def check_validity(a_string)
widgets.gemObjectMethod2(a_string).empty?
end
private
def widgets
#widgets ||= #instance_variable1.gemObjectMethod1
end
Your class still knows a bit too much about the gem object, but now you have broken it down in such a way that you could refactor how you find widgets -- perhaps a different gem or your own implementation of it. For the purposes of your testing, you can isolate that decision from the test by mocking widgets.
let(:gem_widgets) do
instance_double(GemObjectMethod1ResultClass, gemObjectMethod2: true)
end
before do
allow(subject).to receive(:widgets).and_return(gem_widgets)
allow(gem_widgets).to receive(:gemObjectMethod2).with("sample").
and_return([])
end
it 'should pass with "sample"' do
expect(actual).to eql true
end

Disambiguate Function calls in Ruby

I am working through Learn Ruby The Hard Way and came across something intriguing in exercise 49.
In parser.rb I have a function named skip(word_list, word_type) at the top level, which is used to skip through unrequited words (such as stop words) in user input. It is not encapsulated in a class or module. As per the exercise I have to write a unit test for the parser.
This is my code for the Unit Tests:
require "./lib/ex48/parser"
require "minitest/autorun"
class TestGame < Minitest::Test
def test_skip()
word_list = [['stop', 'from'], ['stop', 'the'], ['noun', 'west']]
assert_equal(skip(word_list, 'stop'), nil)
assert_equal(skip([['noun', 'bear'], ['verb', 'eat'], ['noun', 'honey']], 'noun'), nil)
end
end
However, when I run rake test TESTOPTS="-v" from the command line, these particular tests are skipped. This seems to be because there is a clash with the skip method in the Minitest module because they run perfectly after I change the name to skip_words.
Can someone please explain what is going on here exactly?
"Top level functions" are actually methods too, in particular they are private instance methods on Object (there's some funkiness around the main object but that's not important here)
However minitest's Test class also has a skip method and since the individual tests are instance methods on a subclass of Test you end up calling that skip instead.
There's not a very simple way of dealing with this - unlike some languages there is no easy way of saying that you want to call a particular superclass' implementation of something
Other than renaming your method, you'll have to pick an alternative way of calling it eg:
Object.new.send(:skip, list, type)
Object.instance_method(:skip).bind(self).call(list, type)
Of course you can wrap this in a helper method for your test or even redefine skip for this particular Test subclass (although that might lead to some head scratching the day someone tries to call minitest's skip.

Rspec - How to write specs for a chain of methods

I'm learning rspec, and I'm wondering what the most effective way to write specs for a method that calls a chain of other methods. For example:
class Example1
def foo(dependency)
dependency.bar("A")
dependency.baz("B")
dependency.bzz("C")
end
end
Ideally I would like to write specs like this:
it "should call bar" do
ex = Example1.new
dep = mock
dep.should_receive(:bar).with("A")
ex.foo(dep)
end
it "should call baz"
...
it "should call bzz"
...
When I do that, however, I (understandably) get exceptions like 'unexpected method call baz'.
So what's the best way to deal with that? I have come up with a couple of ideas but I don't know if any of them are good.
Make the mock dependency an "as_null_object" so it ignores the extra calls. (Down side - if I was calling unwanted random stuff on that object, I wouldn't know it)
Stub out the two unused dependency method calls in each spec (Down side - feels very DRY)
Stub out all three dependency calls in a 'before' (Down side - puts a lot of junk in the 'before')
It sounds like you have already worked out which options RSpec gives you. I would go with option 1 and use as_null_object. It's true that you might be missing other random method calls on that object but I would be ok with that if the point of each of these tests was simply to assert that a particular method was being called, especially if I have higher level integration tests covering this method.
If you really need to verify that no other methods are called on dependency then option 3 may make sense but such tests can be brittle when implementation changes.
As an aside, to make your test a little simpler you can use subject to avoid explicitly instantiating Example1 (assuming you are using a describe Example1 block), e.g.:
subject.foo(dep)
(However see discussion in comments - an implicit subject can hide intention).
RSpec has a feature called stub_chain: https://www.relishapp.com/rspec/rspec-mocks/v/2-0/docs/stubs/stub-a-chain-of-methods
What about testing them all in one example?
it "should call bar"
ex = Example1.new
dep = mock
dep.should_receive("bar").with("A")
dep.should_receive("baz").with("B")
dep.should_receive("bzz").with("C")
ex.foo(dep)
end
I believe you can use RSpec to verify the order in which they are called, if that matters.
However, this kind of approach often indicate that there is a problem with how the code is written, e.g. a Law Of Demeter violation. In your example, foo should be a methed on the dependency's class.
I would test this code in this way:
describe "checking foo method" do
before(:each) do
#ex = Example1.new
#test = ClassOfDependency.any_instance
#test.as_null_object
end
after(:each) do
#ex.foo(dependency)
end
it "should call bar method" do
#test.should_receive(:bar).with("A")
end
it "should call baz method" do
#test.should_receive(:baz).with("B")
end
it "should call bzz method" do
#test.should_receive(:bzz).with("C")
end
end
But I'm not sure that it will work, hope it'll give you some ideas.

Mock a `puts` to a file in Rspec

I have a silly "queue-class[1]" with the following method, that I want to spec out with Rspec. I am not interested in testing if writing to the file-system works (It works, my computer works) but in whether or not the correct data gets written away.
def write transaction
File.open("messages/#{#next_id}", "w") {|f| f.puts transaction }
#next_id += 1
end
The spec for testing this is:
describe TransactionQueue do
context "#write" do
it "should write positive values" do
open_file = mock File
open_file.stub(:puts)
File.any_instance.stub(:open).and_yield(open_file)
File.any_instance.should_receive(:open)
open_file.should_receive(:puts).with("+100")
#queue = TransactionQueue.new
#queue.write("+100")
end
end
end
Running this, fails, because my Mocks never receive the expected "open" and "puts" messages.
Can I mock File this way? Did I use the any_instance correctly; is my attempt to stub a "block-yield" correct?
I'd rather not use extra gems like FakeFS when it can be avoided; this is not so much about getting it to work; bu mostly about actually understanding what is going on. Hence my attempt to avoid extra gems/layers of complexity.
[1] Class is from The Cucumber Book; but these tests have littel to do with Cucumber itself. I somehow broke the code when following the book; and want to find out what, by writing unit-tests for the parts that the book does not write tests for: the helper classes.
It's not "any instance" of the File class that you expect to receive the open method; it's the File class itself:
File.stub(:open).and_yield(open_file)
File.should_receive(:open)
Furthermore, don't use both a stub and an expectation. If you want to verify that File.open is actually called:
File.should_receive(:open).and_yield(open_file)
If you merely want to stub the open method in case it gets called, but don't want to require it as behaviour of the #queue.write method:
File.stub(:open).and_yield(open_file)
(This is from memory, I haven't used RSpec for a few months.)

Resources