Ruby TDD and Rspec - ruby

Im new to testing in ruby with Rspec. I'm just wanting to write a simple test to see if the below code works. Im not sure how to do it. The code returns an acronym of a given string. thanks
def acronym(sentence)
first_letters = []
sentence.split.each do |word|
first_letters << word[0]
end
first_letters.join
end
describe "acro method" do
it "returns acronym of words" do
end
end

Define Your Input and Expected Output
The point of TDD is to test expected behavior. To construct a test, you must define both your fixture (a known input value) and your expectation (the output you expect your method to produce given a known input value). You then compare the results of your spec with a suitable matcher. For example:
def acronym(sentence)
first_letters = []
sentence.split.each do |word|
first_letters << word[0]
end
first_letters.join
end
describe "#acronym" do
let(:sentence) { 'A very short sentence.' }
it "returns initial letter of each word" do
expect(acronym sentence).to eq('Avss')
end
end
When you run the spec in document format, it should read fairly naturally.
$ rspec --format doc foo_spec.rb
#acronym
returns initial letter of each word
Finished in 0.0017 seconds (files took 0.12358 seconds to load)
1 example, 0 failures
If you change your test's expected output from Avss to avss, then your expectation will fail. A well-written test will give you a useful error like:
Failures:
1) #acronym returns initial letter of each word
Failure/Error: expect(acronym sentence).to eq('avss')
expected: "avss"
got: "Avss"
(compared using ==)
You can then fix your class or method until the desired behavior is achieved.

Use RSpec matchers to check that what your method outputs actually matches what you expect it to do.
https://relishapp.com/rspec/rspec-expectations/docs/built-in-matchers
describe "acro method" do
it "returns acronym of words" do
test_sentence = "this is a test acronym"
expected_acronym = "tiata"
expect(acronym(test_sentence)).to eq(expected_acronym)
end
end

Related

Is there a way in RSpec to assert both number of calls and the list of arguments together?

I want to assert that a certain method is called exactly N times (no more, no less) with specific arguments with a specific order. Also I don't want to actually execute this method so first I stub it with allow().
Suppose I have this code:
class Foo
def self.hello_three_times
Foo.hello(1)
Foo.hello(2)
Foo.hello(3)
end
def self.hello(arg)
puts "hello #{arg}"
end
end
I want to test method hello_three_times that it actually calls hello three times with 1, 2, and 3 as arguments. (And I don't want to really call hello in tests because in reality it contains side effects and is slow.)
So, if I write this test
RSpec.describe Foo do
subject { Foo.hello_three_times }
it do
allow(Foo).to receive(:hello).and_return(true)
expect(Foo).to receive(:hello).with(1).once.ordered
expect(Foo).to receive(:hello).with(2).once.ordered
expect(Foo).to receive(:hello).with(3).once.ordered
subject
end
end
it passes but it doesn't guarantee there are no additional calls afterwards. For example, if there is a bug and method hello_three_times actually looks like this
def self.hello_three_times
Foo.hello(1)
Foo.hello(2)
Foo.hello(3)
Foo.hello(4)
end
the test would still be green.
If I try to combine it with exactly(3).times like this
RSpec.describe Foo do
subject { Foo.hello_three_times }
it do
allow(Foo).to receive(:hello).and_return(true)
expect(Foo).to receive(:hello).exactly(3).times
expect(Foo).to receive(:hello).with(1).once.ordered
expect(Foo).to receive(:hello).with(2).once.ordered
expect(Foo).to receive(:hello).with(3).once.ordered
subject
end
end
it fails because RSpec seems to be treating the calls as fulfilled after the first expect (probably in this case it works in such a way that it expects to have 3 calls first, and then 3 more calls individually, so 6 calls in total):
Failures:
1) Foo is expected to receive hello(3) 1 time
Failure/Error: expect(Foo).to receive(:hello).with(1).once.ordered
(Foo (class)).hello(1)
expected: 1 time with arguments: (1)
received: 0 times
Is there a way to combine such expectations so that it guarantees there are exactly 3 calls (no more, no less) with arguments being 1, 2, and 3 (ordered)?
Oh, I think I found the solution. I can use a block for that:
RSpec.describe Foo do
subject { Foo.hello_three_times }
let(:expected_arguments) do
[1, 2, 3]
end
it do
allow(Foo).to receive(:hello).and_return(true)
call_index = 0
expect(Foo).to receive(:hello).exactly(3).times do |argument|
expect(argument).to eq expected_arguments[call_index]
call_index += 1
end
subject
end
end
It gets the job done guaranteeing there are exactly 3 calls with correct arguments.
It doesn't look very pretty though (introducing that local variable call_index, ugh). Maybe there are prettier solutions out of the box?
You can loop through the expected values and call the expectation for each one like this. You can do the same thing for the allow to ensure that it is only being called with the args that you want, or just keep the allow as you had it to allow anything.
RSpec.describe Foo do
subject { Foo.hello_three_times }
let(:expected_args){ [1, 2, 3] }
before do
expected_args.each do |arg|
allow(Foo).to receive(:hello).with(arg).and_return(true)
end
end
it 'calls the method the expected times' do
expected_args.each do |arg|
expect(Foo).to receive(:hello).with(arg).once
subject
end
end
end

How to test for stdout that is inside a .each iteration block, using rspec?

I have a sample code:
def print_stuff(first_num, second_num)
puts 'hello'
(first_num..second_num).to_a.each do |num|
puts 'The current number is: '
puts "#{num}"
end
end
I and using rspec, I would like to test to see if the output is correct or not. My attempt are as follows:
expect(initialize_program(1, 3)).to output(
"The current number is:
1
The current number is:
2
The current number is:
3").to_stdout
But instead, I get a expected block to output to stdout but not a block error since the initialize_program(1,3) is outputting the texts, but it is done inside a .each block thus the method itself returns the array of range of numbers.
How can I test for the texts inside the block, to see if the outputted texts are correct?
Thanks!
Todd's answer is fine and I'd strongly recommend you read it carefully: refactor your app in a way that UI (CLI in your case) is minimal and easy to test. But when you want full coverage you'd need to test that std output eventually.
The way you're using it now:
expect(initialize_program(1, 3)).to output("whatever").to_stdout
Means that initialize_program(1, 3) is evaluated immediately when the matcher is called, and it's too soon - it has to do it's magic(*) first, and then run your code. Try like this:
expect { initialize_program(1, 3) }.to output("whatever").to_stdout
Now, instead passing results of calling initialize_program(1, 3) into the matcher, you pass a block that "knows how" to call initialize_program(1, 3). So what the matcher does:
saves the block for later
does it magic to capture whatever goes to the STDOUT (see below)
calls the block, calling the initialize_program(1, 3), capturing whatever was supposed to go to STDOUT
compares it with what you've set up in your expectation (the output("whatever") part)
https://relishapp.com/rspec/rspec-expectations/docs/built-in-matchers/output-matcher
The mentioned magic is not that magical anyway:
https://github.com/rspec/rspec-expectations/blob/44d90f46a2654ffeab3ba65eff243039232802ce/lib/rspec/matchers/built_in/output.rb#L49
and
https://github.com/rspec/rspec-expectations/blob/44d90f46a2654ffeab3ba65eff243039232802ce/lib/rspec/matchers/built_in/output.rb#L141
It just creates StringIO, and replaces global var $stdout with it.
Refactor to Inspect a String, Not Standard Output
This type of code is why you should write your tests first. You're essentially testing Kernel#puts, which always returns nil, rather than validating that you've constructed the String you expect. Don't do that. Instead, refactor like so:
def print_stuff(num1, num2)
str =
(num1..num2).map { |num|"The current number is: #{num}" }
.join "\n"
puts str
str
end
print_stuff 1, 3
#=> "The current number is: 1\nThe current number is: 2\nThe current number is: 3"
This will not only print what you expect on standard output:
The current number is: 1
The current number is: 2
The current number is: 3
but will also use the implicit return of the last line of your method to return a value that you can use to compare to the expectations in your spec.
You might also refactor the method to return an Array of String objects, or whatever else you might explicitly want to test. The more your real method reflects what you plan to test, the better.
RSpec Examples
RSpec.describe '#print_stuff' do
it 'prints the expected message' do
expected_string = <<~EOF
The current number is: 1
The current number is: 2
The current number is: 3
EOF
expect(print_stuff 1, 3).to eql(expected_string.chomp)
end
# Even without the collection matchers removed in RSpec 3,
# you can still validate the number of items returned.
it 'returns the expected number of lines' do
lines = print_stuff(1, 3).split("\n").count
expect(lines).to eql(3)
end
end
Testing RSpec Examples in IRB
In irb, you can validate your specs like so:
require 'rspec'
include RSpec::Matchers
expected_string = <<~EOF
The current number is: 1
The current number is: 2
The current number is: 3
EOF
# String#chomp is needed to strip the newline from the
# here-document
expect(print_stuff 1, 3).to eql(expected_string.chomp)
# test the returned object in other ways, if you want
lines = print_stuff(1, 3).split("\n").count
expect(lines).to eql(3)

Is it possible to create a new RSpec matcher by composing existing ones?

In one of my specs, I find myself repeating lines like these often:
expect(result.status).to be(:success)
expect(result.offers).not_to be_empty
expect(result.comments).to be_empty
To make my tests more succinct and readable, I want to compose these into a line like this:
expect(result).to be_successful
I can do this by creating a custom matcher:
matcher :be_successful do
match { |result|
result.status == :success &&
result.offers.length > 0 &&
result.comments.empty?
}
end
But I now have a failing test, and the failure message is completely useless. All it says now is Expected #<Result ...> to be successful.
I know I can override the failure message, but now this solution is getting more complicated than it's worth for saving 2 lines for every spec example. The original 3 lines generated useful failure messages, all I wanted to do was combine them into 1 line.
I could move the 3 lines into a separate function (e.g. assert_successful) and call that from each spec example, but I'd like to keep the matcher syntax.
Can this be done?
According to this
You could do something like this:
RSpec::Matchers.define :be_successful do
match do |result|
result.status == :success &&
result.offers.length > 0 &&
result.comments.empty?
end
failure_message do |result|
"Should have #{result} equal to be successful"
end
failure_message_when_negated do |result|
"Should not have #{result} to be successful"
end
end
If you reuse this test elsewhere more than 3 times, then it makes sense to create a new matcher and override the failure messages(It's not an overhead). If you use this test only one time then it makes sense to keep it without overly abstract it.
One way of organizing this is: Put the real testing code into
a method _check_ok(expected, actual) of its own,
which returns either [true] or [false, message].
This is called as follows:
RSpec::Matchers.defined :check_ok do |expected|
match do |actual|
return _check_ok(expected, actual)[0]
end
failure_message do |actual|
return _check_ok(expected, actual)[1]
end
end
This repeats the _check_ok call in the failure case,
which normally isn't a problem.

How do I call a method from an rspec test

I'm learning Ruby and TDD (rspec) at the same time.
I've written the following test:
describe is_eligible do
it "returns true if the passed in string is not part of a list" do
result = is_eligible("abc")
result.should eq(false)
end
end
It is testing the following code:
def is_eligible(team_name)
array = Array.new
array << "abc" << "def" << "ghi"
if array.include?(team_name)
return false
else
return true
end
end
I'm receiving the following error and can't find out why.
*/Users/joel.dehlin/top32/lib/ineligible_teams.rb:6:in `is_eligible': wrong number of arguments (0 for 1) (ArgumentError)*
Any help is appreciated!
The problem is that the describe method expects a string or something that can evaluate to string. If you say "is_eligible" without the quotes, it will actually try to call the method and you get the error.
describe "is_eligible" do
it "returns true if the passed in string is not part of a list" do
result = is_eligible("abc")
result.should eq(false)
end
end

Generating example IDs in rspec

I need help figuring out how to generate a unique identifier for each example in my rspec tests. What do I change for the below code to work?
describe 'Verify that my server' do
#x = 1
it "does something " + #x.to_s do
2.should==2
end
it "does something else " + #x.to_s do
2.should==2
end
after(:each) do
#x+=1
end
end
Take a look at ffaker for generating random values in tests. It can generate real-looking random data, like e-mail addresses, IP addresses, phone numbers, people's names etc, but it also has basic methods for generating random strings of letters and numbers.
Faker.numerify("###-###-###")
# => 123-456-789
Alternatively you can use stdlib's SecureRandom.
Each example in your Rspec should complete a sentence, a sentence you usually start in a describe block encapsulating related tests.
I took this from one of my own specs:
describe Redis::BigHash do
before :each do
#hash = Redis::BigHash.new
#hash[:foo] = "bar"
#hash[:yin] = "yang"
end
describe "#[]" do
it "should read an existing value" do
#hash[:foo].should == "bar"
end
it "should get nil for a value that doesn't exist" do
#hash[:bad_key].should be_nil
end
it "should allow lookup of multiple keys, returning an array" do
#hash[:foo, :yin, :bad_key].should == ["bar", "yang", nil]
end
end
end
You end up with sentences like:
Redis::BigHash#[] should read an existing value.
Redis::BigHash#[] should get nil for a value that doesn't exist.
Redis::BigHash#[] should allow lookup of multiple keys, returning an array.
Just simple English sentences that describe the behavior you want.

Resources