First attempt in Ruby Rspec - ruby

I am new to Ruby and Rspec, and so I happened to found this bit of code:
Here is my Specification:
RSpec.describe Surveyor::Answer, '03: Answer validations' do
context "for a free text question" do
let(:question) { double(Surveyor::Question, type: 'free_text') }
# NOTE: The rating validations should not apply for 'free_text' questions.
subject { described_class.new(question: question, value: 'anything') }
it { should be_valid }
end
Here is my Class:
module Surveyor
class Answer
def initialize(question_answer)
#question = question_answer[:question]
#answer = question_answer[:value]
end
def question_type
# I want to check what is the type of question here.
# 'free_text' or 'rating'
# if free_text
# print question type
# else
# do something
end
end
My question is how can I print(puts) the type of question (free_text/rating) in Answer class?
When I tried using print question_answer[:question]it only gave me #<Double Surveyor::Question>
So I could not use question_answer[:question][:type]

You can access the type in the constructor simply: question_answer[:question].type, or later in object level methods: #question.type.
You can't access it like question_answer[:question][:type] because the double method in the test creates a classic like object rather than a hash.
A tip: when a method accepts parameters as a single hash you can simply name that as options or params but if you have only 3-4 params, you can use separate variables for params instead of a hash

Related

How do I tack a string onto a variable and evaluated the entire thing as a variable in Ruby?

I have the following Ruby code:
module BigTime
FOO1_MONEY_PIT = 500
FOO2_MONEY_PIT = 501
class LoseMoney
##SiteName = 'FOO1'
#site_num = ##SiteName_MONEY_PIT
def other_unimportant_stuff
whatever
end
end
end
So, what I'm trying to do here is set the SiteName and then use SiteName and combine it with the string _MONEY_PIT so I can access FOO1_MONEY_PIT and store its contents (500 in this case) in #site_num. Of course, the above code doesn't work, but there must be a way I can do this?
Thanks!!
If you want to dynamically get the value of a constant, you can use Module#const_get:
module BigTime
FOO1_MONEY_PIT = 500
FOO2_MONEY_PIT = 501
class LoseMoney
##SiteName = 'FOO1'
#site_num = BigTime.const_get(:"#{##SiteName}_MONEY_PIT")
end
end
Do not, under any circumstance, use Kernel#eval for this. Kernel#eval is extremely dangerous in any context where there is even the slightest possibility that an attacker may be able to control parts of the argument.
For example, if a user can choose the name of the site, and they name their site require 'fileutils'; FileUtils.rm_rf('/'), then Ruby will happily evaluate that code, just like you told it to!
Kernel#eval is very dangerous and you should not get into the habit of just throwing an eval at a problem. It is a very specialized tool that should only be employed when there is no other option (spoiler alert: there almost always is another option), and only after a thorough security review.
Please note that dynamically constructing variable names is already a code smell by itself, regardless of whether you use eval or not. It pretty much always points to a design flaw somewhere. In general, you can almost guaranteed replace the multiple variables with a data structure. E.g. in this case something like this:
module BigTime
MONEY_PITS = {
'FOO1' => 500,
'FOO2' => 501,
}.freeze
class LoseMoney
##SiteName = 'FOO1'
#site_num = MONEY_PITS[##SiteName]
end
end
You can refactor this as to use a Hash for your name lookups, and a getter method to retrieve it for easy testing/validation. For example:
module BigTime
MONEY_PITS = { FOO1: 500, FOO2: 501 }
MONEY_PIT_SUFFIX = '_MONEY_PIT'
class LoseMoney
##site = :FOO1
def initialize
site_name
end
def site_name
#site_name ||= '%d%s' % [MONEY_PITS[##site], MONEY_PIT_SUFFIX]
end
end
end
BigTime::LoseMoney.new.site_name
#=> "500_MONEY_PIT"

RSpec mock method inside of select loop

I want to test simple class which iterate through array of hashes and return only those with status Pending which were updated more than 2 days ago.
class FetchPending
PROJECT_KEY = 'TPFJT'
TWO_DAYS = Time.now - 2 * 24 * 60 * 60
def call
project.select do |issue|
issue.fields.dig('status', 'name') == 'Pending' &&
DateTime.parse(issue.fields.dig('updated')) < TWO_DAYS
end
end
private
def project
#project ||= Jira::ProjectConnection.new(PROJECT_KEY).call
end
end
How to test fields method which is a method of Jira-Ruby gem. I think it comes from here (Field class in resource of gem) because nowhere else have I found fields method.
Here are my thoughts after debugging:
project.class - Array
issue.class - JIRA::Resource::Issue
my natural thinking was:
before do
# (...) some other mocks
allow(JIRA::Resource::Issue).to receive(:fields)
end
But I'm getting an error:
Failure/Error: allow(JIRA::Resource::Issue).to receive(:fields)
JIRA::Resource::Issue does not implement: fields
I have been struggling with this problem for DAYS, I'm pretty desperate here. How to mock this method?
Here is my rest of my specs:
RSpec.describe FetchPending do
subject { described_class.new }
let(:project_hash) do
[
{
'key': 'TP-47',
'fields': {
'status': {
'name': 'Pending'
},
'assignee': {
'name': 'michael.kelso',
'emailAddress': 'michael.kelso#example.com'
},
'updated': '2020-02-19T13:20:50.539+0100'
}
}
]
end
let(:project) { instance_double(Jira::ProjectConnection) }
before do
allow(Jira::ProjectConnection).to receive(:new).with(described_class::PROJECT_KEY).and_return(project)
allow(project).to receive(:call).and_return(project_hash)
allow(JIRA::Resource::Issue).to receive(:fields)
end
it 'return project hash' do
expect(subject.call).include(key[:'TP-47'])
end
and_return is generally used for returning a value (such as a string or an integer) or sequence of values, but for objects you sometimes need use a block. Additionally, if call is a valid method on a Jira::ProjectConnection object that returns the value of project_hash, you can directly mock its behavior when declaring your instance double (this functionality is unclear from the Relish docs bc they are kinda terrible). Something like this will probably work:
let(:project) { instance_double(Jira::ProjectConnection, call: project_hash) }
before do
# Ensure new proj conns always return mocked 'project' obj
allow(Jira::ProjectConnection).to receive(:new).with(
described_class::PROJECT_KEY
) { project }
end
If it still doesn't work, try temporarily replacing described_class::PROJECT_KEY with anything to debug; this can help you confirm if you specified the wrong arg(s) being sent to new.
With regard to the error message, it looks like JIRA::Resource::Issue doesn't have a fields attribute/method, though fields appears to be nested in attrs? The JIRA::Resource::Project#issues method also translates the issues in the JSON into Issue objects, so if you're using that method you will need to change the contents of project_hash.

Test failing - one class method to call another ("expected: 1 time with arguments, received 0 times")

My problem:
I'm trying to stub a class method that returns an instance of that class, but I'm getting the following error for the test entitled "creates an instance with CSV data":
Failures:
1) QuestionData.load_questions creates an instance with CSV data
Failure/Error: expect(question_data_class).to receive(:new).with(data).and_return(question_data_instance)
(QuestionData (class)).new([{:time_limit=>10, :text=>"Who was the legendary Benedictine monk who invented champagne?", :correct_...the world?", :correct_answer=>"Lake Superior", :option_2=>"Lake Victoria", :option_3=>"Lake Huron"}])
expected: 1 time with arguments: ([{:time_limit=>10, :text=>"Who was the legendary Benedictine monk who invented champagne?", :correct_...the world?", :correct_answer=>"Lake Superior", :option_2=>"Lake Victoria", :option_3=>"Lake Huron"}])
received: 0 times
The context:
The code (shown below) works - QuestionData.load_questions loads data from a CSV file and calls QuestionData.new with the data as an argument. My test for the .load_questions method however, is giving the above error. When it's called, the double of the QuestionData class isn't receiving the stub of .new with the data double.
I've tried researching how to test stubs that return another stub or an instance, but can't seem to find a relevant answer.
I'd really appreciate any help or advice, thanks very much in advance!
The code:
require "csv"
class QuestionData
attr_reader :questions
def initialize(questions)
#questions = questions
end
def self.load_questions(file = './app/lib/question_list.csv', questions = [])
self.parse_csv(file, questions)
self.new(questions)
end
def self.parse_csv(file, questions)
CSV.foreach(file) do |row|
time_limit, text, correct_answer, option_2, option_3 = row[0],
row[1], row[2], row[3], row[4]
questions << { time_limit: time_limit, text: text,
correct_answer: correct_answer, option_2: option_2, option_3: option_3
}
end
end
end
The test file:
require './app/models/question_data'
describe QuestionData do
subject(:question_data_instance) { described_class.new(data) }
let(:question_data_class) { described_class }
let(:CSV) { double(:CSV, foreach: nil) }
let(:questions) { [] }
let(:file) { double(:file) }
let(:data) do
[{
time_limit: 10,
text: "Who was the legendary Benedictine monk who invented champagne?",
correct_answer: "Dom Perignon",
option_2: "Ansgar",
option_3: "Willibrord"
},
{
time_limit: 12,
text: "Name the largest freshwater lake in the world?",
correct_answer: "Lake Superior",
option_2: "Lake Victoria",
option_3: "Lake Huron"
}]
end
describe '#questions' do
it "has an array of question data from CSV" do
expect(question_data_instance.questions).to eq(data)
end
end
describe '.parse_csv' do
it "parses CSV data into hash data" do
expect(CSV).to receive(:foreach).with(file)
question_data_class.parse_csv(file, questions)
end
end
describe '.load_questions' do
it "calls '.parse_csv' method" do
expect(question_data_class).to receive(:parse_csv).with(file, questions)
question_data_class.load_questions(file, questions)
end
it "creates an instance with CSV data" do
allow(question_data_class).to receive(:load_questions).with(file, questions).and_return(question_data_instance)
allow(question_data_class).to receive(:new).with(data).and_return(question_data_instance)
expect(question_data_class).to receive(:new).with(data).and_return(question_data_instance)
question_data_class.load_questions(file, questions)
end
end
describe '.new' do
it "creates a new instance with CSV data" do
expect(question_data_class).to receive(:new).with(data).and_return(question_data_instance)
question_data_class.new(data)
end
end
end
The thing is that you are stubbing the call on:
allow(question_data_class).to receive(:load_questions).with(file)
If you still want that the call executes you need to add a:
and_call_original
Therefore the original method will be executed and your code will call the new method on the original block.
But the thing is that you don't need to stub the class you just need to change the stubs because you are calling the method on a double, and it will try to execute it in a class, so you might need to change your second test to:
describe '.load_questions' do
it "creates an instance containing CSV data" do
expect(described_class).to receive(:new).with(data).and_return(question_data_instance)
described_class.load_questions(file)
end
end

List dynamic attributes in a Mongoid Model

I have gone over the documentation, and I can't find a specific way to go about this. I have already added some dynamic attributes to a model, and I would like to be able to iterate over all of them.
So, for a concrete example:
class Order
include Mongoid::Document
field :status, type: String, default: "pending"
end
And then I do the following:
Order.new(status: "processed", internal_id: "1111")
And later I want to come back and be able to get a list/array of all the dynamic attributes (in this case, "internal_id" is it).
I'm still digging, but I'd love to hear if anyone else has solved this already.
Just include something like this in your model:
module DynamicAttributeSupport
def self.included(base)
base.send :include, InstanceMethods
end
module InstanceMethods
def dynamic_attributes
attributes.keys - _protected_attributes[:default].to_a - fields.keys
end
def static_attributes
fields.keys - dynamic_attributes
end
end
end
and here is a spec to go with it:
require 'spec_helper'
describe "dynamic attributes" do
class DynamicAttributeModel
include Mongoid::Document
include DynamicAttributeSupport
field :defined_field, type: String
end
it "provides dynamic_attribute helper" do
d = DynamicAttributeModel.new(age: 45, defined_field: 'George')
d.dynamic_attributes.should == ['age']
end
it "has static attributes" do
d = DynamicAttributeModel.new(foo: 'bar')
d.static_attributes.should include('defined_field')
d.static_attributes.should_not include('foo')
end
it "allows creation with dynamic attributes" do
d = DynamicAttributeModel.create(age: 99, blood_type: 'A')
d = DynamicAttributeModel.find(d.id)
d.age.should == 99
d.blood_type.should == 'A'
d.dynamic_attributes.should == ['age', 'blood_type']
end
end
this will give you only the dynamic field names for a given record x:
dynamic_attribute_names = x.attributes.keys - x.fields.keys
if you use additional Mongoid features, you need to subtract the fields associated with those features:
e.g. for Mongoid::Versioning :
dynamic_attribute_names = (x.attributes.keys - x.fields.keys) - ['versions']
To get the key/value pairs for only the dynamic attributes:
make sure to clone the result of attributes(), otherwise you modify x !!
attr_hash = x.attributes.clone #### make sure to clone this, otherwise you modify x !!
dyn_attr_hash = attr_hash.delete_if{|k,v| ! dynamic_attribute_names.include?(k)}
or in one line:
x.attributes.clone.delete_if{|k,v| ! dynamic_attribute_names.include?(k)}
So, what I ended up doing is this. I'm not sure if it's the best way to go about it, but it seems to give me the results I'm looking for.
class Order
def dynamic_attributes
self.attributes.delete_if { |attribute|
self.fields.keys.member? attribute
}
end
end
Attributes appears to be a list of the actual attributes on the object, while fields appears to be a hash of the fields that were predefined. Couldn't exactly find that in the documentation, but I'm going with it for now unless someone else knows of a better way!
try .methods or .instance_variables
Not sure if I liked the clone approach, so I wrote one too. From this you could easily build a hash of the content too. This merely outputs it all the dynamic fields (flat structure)
(d.attributes.keys - d.fields.keys).each {|a| puts "#{a} = #{d[a]}"};
I wasn't able to get any of the above solutions to work (as I didn't want to have to add slabs and slabs of code to each model, and, for some reason, the attributes method does not exist on a model instance, for me. :/), so I decided to write my own helper to do this for me. Please note that this method includes both dynamic and predefined fields.
helpers/mongoid_attribute_helper.rb:
module MongoidAttributeHelper
def self.included(base)
base.extend(AttributeMethods)
end
module AttributeMethods
def get_all_attributes
map = %Q{
function() {
for(var key in this)
{
emit(key, null);
}
}
}
reduce = %Q{
function(key, value) {
return null;
}
}
hashedResults = self.map_reduce(map, reduce).out(inline: true) # Returns an array of Hashes (i.e. {"_id"=>"EmailAddress", "value"=>nil} )
# Build an array of just the "_id"s.
results = Array.new
hashedResults.each do |value|
results << value["_id"]
end
return results
end
end
end
models/user.rb:
class User
include Mongoid::Document
include MongoidAttributeHelper
...
end
Once I've added the aforementioned include (include MongoidAttributeHelper) to each model which I would like to use this method with, I can get a list of all fields using User.get_all_attributes.
Granted, this may not be the most efficient or elegant of methods, but it definitely works. :)

How to share data between implementation and description of a spec?

I wonder if there's any good way to reuse data between implementation and description of a spec... More particularly, I'd like to be able do something like the following:
describe "#some_method" do
let(:arg1) { "Sample String 1" }
let(:arg2) { "Sample String 2" }
context "with '#{arg1}', its result" do
specify { some_method(arg1).should == 1 }
end
context "with '#{arg2}', its result" do
specify { some_method(arg2).should == 2 }
end
end
Of course, this code won't work - arg1 and arg2 are not accessible outside of spec bodies.
Is it possible to achieve the similar result without using global variables or external classes?
Update:
I'm interested in the output of the spec. Something like this:
#some_method
with 'Sample String 1' its result
should == 1
with 'Sample String 2' its result
should == 2
The answer is that you don't use dynamic descriptions. The RSpec way to do this would be
describe "#some_method" do
it "extracts the number correctly" do
some_method("Sample String 1").should == 1
some_method("Sample String 2").should == 2
end
end
It is no problem to hard-code test data in your specs. If you want more complete output, you can use a custom matcher
require 'rspec'
class Test
def some_method(str)
str[/[0-9]+/].to_i
end
end
RSpec::Matchers.define :return_value_for_argument do |result, arg|
match do |actual|
actual.call(arg) == result
end
description do
"return #{result.inspect} for argument #{arg.inspect}"
end
end
describe Test do
let(:test) { Test.new }
describe "#some_method" do
subject { test.method(:some_method) }
it { should return_value_for_argument 1, "str 1" }
end
end
When doing API testing, I find it incredibly useful to be able to see the path, params, and response of each test. I have used the very useful tips given by Jeff Nyman to store things in the example.metatadata[:thing_i_want_to_store_like_url] of each test and with a custom formatter, print it out.
So my tests output look something like this:
that jonathan does not know it exists
:path : /user/20
:params: {}
=> response: {"error"=>{"message"=>"error", "code"=>404}}
that jonathan cannot edit
:path : /user/20/update
:params: {:name=>"evil_name"}
=> response: {"error"=>{"message"=>"error", "code"=>404}}
It's not appropriate to cite specific arguments in your descriptions. Your descriptions should provide a human-readable description of the desired behavior, without reference to specific arguments in most cases.

Resources