is there a proper way to use is_a? with an instance_double? - ruby

I have real world code which does something like:
attr_reader :response
def initialize(response)
#response = response
end
def success?
response.is_a?(Net::HTTPOK)
end
and a test:
subject { described_class.new(response) }
let(:response) { instance_double(Net::HTTPOK, :body => 'nice body!', :code => 200) }
it 'should be successful' do
expect(subject).to be_success
end
This fails because #<InstanceDouble(Net::HTTPOK) (anonymous)> is not a Net::HTTPOK
... The only way I have been able to figure out how to get around this is with quite the hack attack:
let(:response) do
instance_double(Net::HTTPOK, :body => 'nice body!', :code => 200).tap do |dbl|
class << dbl
def is_a?(arg)
instance_variable_get('#doubled_module').send(:object) == arg
end
end
end
end
I can't imagine that I am the only one in the history ruby and rspec that is testing code being that performs introspection on a test double, and therefore think there has got to be a better way to do this-- There has to be a way that is_a? just will work out the box with a double?

I would do:
let(:response) { instance_double(Net::HTTPOK, :body => 'nice body!', :code => 200) }
before { allow(response).to receive(:is_a?).with(Net::HTTPOK).and_return(true) }

Related

How to test WebSockets For Hanami?

Using the following:
Hanami cookbook websockets
IoT Saga - Part 3 - Websockets! Connecting LiteCable to Hanami
I've been able to add WebSockets to Hanami, however as this is for production code I want to add specs; but I can't find information on how to test WebSockets and Hanami using Rspec.
I've been able to find this for RoR but nothing non-Rails specific or Hanami Specific, I have asked on the Hanami Gitter but not gotten a response yet.
Is the TCR gem the only way? I would prefer something simpler but If I must how would I set it up for anycable-go via litecable.
How can I test WebSockets for Hanami using Rspec?
To get this working requires several moving parts, the first is the Socket simulator which simulates the receiving socket on the webserver:
Note: url_path should be customized to what works for your web socket specific endpoint
# frozen_string_literal: true
require 'puma'
require 'lite_cable/server'
require_relative 'sync_client'
class SocketSimulator
def initialize(x_site_id_header: nil)
#server_logs = []
#x_site_id_header = x_site_id_header
end
attr_accessor :server_logs
def client
return #client if #client
url_path = "/ws?connection_token=#{connection_token}"
#client = SyncClient.new("ws://127.0.0.1:3099#{url_path}", headers: headers, cookies: '')
end
def connection_token
#connection_token ||= SecureRandom.hex
end
def user
return #user if #user
email = "#{SecureRandom.hex}#mailinator.com"
password = SecureRandom.hex
#user = Fabricate.create :user, email: email, site_id: site_id, password: password
end
def start
#server = Puma::Server.new(
LiteCable::Server::Middleware.new(nil, connection_class: Api::Sockets::Connection),
Puma::Events.strings
).tap do |server|
server.add_tcp_listener '127.0.0.1', 3099
server.min_threads = 1
server.max_threads = 4
end
#server_thread = Thread.new { #server.run.join }
end
def teardown
#server&.stop(true)
#server_thread&.join
#server_logs.clear
end
def headers
{
'AUTHORIZATION' => "Bearer #{jwt}",
'X_HANAMI_DIRECT_BOOKINGS_SITE_ID' => #x_site_id_header || site_id
}
end
def site_id
#site_id ||= SecureRandom.hex
end
def jwt
#jwt ||= Interactors::Users::GenerateJwt.new(user, site_id).call.jwt
end
end
The next thing is the SyncClient which is a fake client you can use to actually connect to the simulated socket:
# frozen_string_literal: true
# Synchronous websocket client
# Copied and modified from https://github.com/palkan/litecable/blob/master/spec/support/sync_client.rb
class SyncClient
require 'websocket-client-simple'
require 'concurrent'
require 'socket'
WAIT_WHEN_EXPECTING_EVENT = 5
WAIT_WHEN_NOT_EXPECTING_EVENT = 0.5
attr_reader :pings
def initialize(url, headers: {}, cookies: '')
#messages = Queue.new
#closed = Concurrent::Event.new
#has_messages = Concurrent::Semaphore.new(0)
#pings = Concurrent::AtomicFixnum.new(0)
#open = Concurrent::Promise.new
#ws = set_up_web_socket(url, headers.merge('COOKIE' => cookies))
#open.wait!(WAIT_WHEN_EXPECTING_EVENT)
end
def ip
Socket.ip_address_list.detect(&:ipv4_private?).try(:ip_address)
end
def set_up_web_socket(url, headers)
WebSocket::Client::Simple.connect(
url,
headers: headers
) do |ws|
ws.on(:error, &method(:on_error))
ws.on(:open, &method(:on_open))
ws.on(:message, &method(:on_message))
ws.on(:close, &method(:on_close))
end
end
def on_error(event)
event = RuntimeError.new(event.message) unless event.is_a?(Exception)
if #open.pending?
#open.fail(event)
else
#messages << event
#has_messages.release
end
end
def on_open(_event = nil)
#open.set(true)
end
def on_message(event)
if event.type == :close
#closed.set
else
message = JSON.parse(event.data)
if message['type'] == 'ping'
#pings.increment
else
#messages << message
#has_messages.release
end
end
end
def on_close(_event = nil)
#closed.set
end
def read_message
#has_messages.try_acquire(1, WAIT_WHEN_EXPECTING_EVENT)
msg = #messages.pop(true)
raise msg if msg.is_a?(Exception)
msg
end
def read_messages(expected_size = 0)
list = []
loop do
list_is_smaller = list.size < expected_size ? WAIT_WHEN_EXPECTING_EVENT : WAIT_WHEN_NOT_EXPECTING_EVENT
break unless #has_messages.try_acquire(1, list_is_smaller)
msg = #messages.pop(true)
raise msg if msg.is_a?(Exception)
list << msg
end
list
end
def send_message(message)
#ws.send(JSON.generate(message))
end
def close
sleep WAIT_WHEN_NOT_EXPECTING_EVENT
raise "#{#messages.size} messages unprocessed" unless #messages.empty?
#ws.close
wait_for_close
end
def wait_for_close
#closed.wait(WAIT_WHEN_EXPECTING_EVENT)
end
def closed?
#closed.set?
end
end
The last part is a fake channel to test against:
# frozen_string_literal: true
class FakeChannel < Api::Sockets::ApplicationChannel
identifier :fake
def subscribed
logger.info "Can Reject? #{can_reject?}"
reject if can_reject?
logger.debug "Streaming from #{stream_location}"
stream_from stream_location
end
def unsubscribed
transmit message: 'Goodbye channel!'
end
def can_reject?
logger.info "PARAMS: #{params}"
params.fetch('value_to_check', 0) > 5
end
def foo
transmit('bar')
end
end
To use in specs:
# frozen_string_literal: true
require_relative '../../../websockets-test-utils/fake_channel'
require_relative '../../../websockets-test-utils/socket_simulator'
RSpec.describe Interactors::Channels::Broadcast, db_truncation: true do
subject(:interactor) { described_class.new(token: connection_token, loc: 'fake', message: message) }
let(:identifier) { { channel: 'fake' }.to_json }
let(:socket_simulator) { SocketSimulator.new }
let(:client) { socket_simulator.client }
let(:user) { socket_simulator.user }
let(:connection_token) { socket_simulator.connection_token }
let(:channel) { 'fake' }
let(:message) { 'woooooo' }
before do
socket_simulator.start
end
after do
socket_simulator.teardown
end
describe 'call' do
before do
client.send_message command: 'subscribe',
identifier: identifier
end
it 'broadcasts a message to the correct channel' do
expect(client.read_message).to eq('type' => 'welcome')
expect(client.read_message).to eq(
'identifier' => identifier,
'type' => 'confirm_subscription'
)
interactor.call
expect(client.read_message).to eq(
'identifier' => identifier,
'message' => message
)
end
context 'with other connection' do
let(:user2) { Fabricate.create :user }
let(:jwt) { Interactors::Users::GenerateJwt.new(user2, site_id).call.jwt }
let(:site_id) { socket_simulator.site_id }
let(:url_path) { "/ws?connection_token=#{SecureRandom.hex}" }
let(:client2) { SyncClient.new("ws://127.0.0.1:3099#{url_path}", headers: {}, cookies: '') }
before do
client2.send_message command: 'subscribe',
identifier: identifier
end
it "doesn't broadcast to connections that shouldn't get it" do
aggregate_failures 'broadcast!' do
expect(client2.read_message).to eq('type' => 'welcome')
expect(client2.read_message).to eq(
'identifier' => identifier,
'type' => 'confirm_subscription'
)
expect(client.read_message).to eq('type' => 'welcome')
expect(client.read_message).to eq(
'identifier' => identifier,
'type' => 'confirm_subscription'
)
interactor.call
sleep 1
expect(client.read_message).to eq(
'identifier' => identifier,
'message' => message
)
expect { client2.close }.not_to raise_exception
end
end
end
end
end

How to use DSL metaprogramming with Sinatra

I'm trying to work on a DSL to manage different locales within the same route, like get "/test". The
This is an exercise to learn how to extend Sinatra, therefore Rack::Locale or a similar tool is not a valid answer.
Based on the body of the request JSON body, assuming I receive JSON as POST or PUT, I want to respond with the specific locale.
I currently have a barebones script, of what I think I need:
class Locale
attr_reader :locale_id
attr_reader :described_class
alias :current_locale :locale_id
def initialize(locale_id, &block)
#locale_id = locale_id
instance_eval &block
end
end
def locale(locale_id, &block)
Locale.new(locale_id, &block)
end
I am missing the capability to respond based on the locale in the request.body JSON I receive as input, and the class here has something else I do not yet see that is needed or is missing.
An example of how this would get used would be:
get '/' do
locale 'cs-CS' do
"Czech"
#or db query or string
end
locale 'en-UK' do
"British english"
#or db query or string
end
end
Therefore to try to clarify even more clearly I will try with a TDD approach:
As User when I send a JSON that contains: "locale": "cs-CS" the result is Czech.
Have you read Extending The DSL and the Conditions section of the README?
Right now, you're not really extending the DSL. I'd redesign it slightly, because it looks like you'd want to match on a case statement but that would mean creating lots of classes or an ugly matching statement. But, Sinatra already has some really nice ways to match on routes and conditions. So, something like this would be more idiomatic:
post '/', :locale => "Czech" do
"Czech"
end
post '/', :locale => "British English" do
"British"
end
or
post '/', :locale => "en-GB" do
"cs-CS"
end
post '/', :locale => "cs-CS" do
"cs-CS"
end
How to do this? First, you'll need a filter to transform the JSON coming in:
before do
if request.media_type == "application/json"
request.body.rewind
#json = JSON.parse request.body.read
#locale = #json["locale"] && Locales[#json["locale"]]
end
end
and then you'll need a condition to check against:
set(:locale) {|value|
condition {
!!#locale && (#locale == value || #json["locale"] == value)
}
}
All together (app.rb):
require 'sinatra'
Locales = {
'cs-CS' => "Czech",
'en-GB' => "British English"
}
before do
if request.media_type == "application/json"
request.body.rewind
#json = JSON.parse request.body.read
#locale = #json["locale"] && Locales[#json["locale"]]
end
end
set(:locale) {|value|
condition {
!!#locale && (#locale == value || #json["locale"] == value)
}
}
post '/', :locale => "en-GB" do
"cs-CS"
end
post '/', :locale => "cs-CS" do
"cs-CS"
end
That works but it won't work as an extension. So, relying the docs I posted at the top:
require 'sinatra/base'
module Sinatra
module Localiser
Locales = {
'cs-CS' => "Czech",
'en-GB' => "British English"
}
def localise!(locales=Locales)
before do
if request.media_type == "application/json"
request.body.rewind
#json = JSON.parse request.body.read
#locale = #json["locale"] && locales[#json["locale"]]
end
end
set(:locale) {|value|
condition {
!!#locale && (#locale == value || #json["locale"] == value)
}
}
end
end
register Localiser
end
Now it will extend the DSL. For example:
require "sinatra/localiser"
class Localised < Sinatra::Base
register Sinatra::Localiser
localise!
post '/', :locale => "Czech" do
"Czech"
end
post '/', :locale => "British English" do
"British"
end
["get","post"].each{|verb|
send verb, "/*" do
"ELSE"
end
}
run! if app_file == $0
end
Hopefully that helps clarify a few things for you.

Writing rspec for controllers with params?

I have a controller which has the following method
class <controllername> < ApplicationController
def method
if params["c"]
.....
elsif params["e"]
.....
else
.....
end
end
end
Now, I want to write rspec for the above code.
How can I write separate context for both the params and how will I mention them as a get method.
If I understand your question correctly, you can try approach like this:
RSpec.describe <controllername>, :type => :controller do
describe "GET my_method" do
context "param 'c' is provided"
get :my_method, { "c" => "sample value" }
expect(response).to have_http_status(:success)
end
context "param 'e' is provided"
get :my_method, { "e" => "sample value" }
expect(response).to have_http_status(:success)
end
end
end
Hope it puts you in proper direction.
Good luck!

Mocha for same method needs to return 2 different values

Using Mocha, I'm stubbing the same method that needs to return 2 separate values. No matter what I do, it only returns 1 of the 2 values, thus 1 of my rspec tests always fail. How do I get the stub to return the correct value at the right time?
The code:
describe "#method" do
it "has something" do
hash = { "allow_sharing" => "1"}
CustomClass.stubs(:app_settings).returns(hash)
get 'method', :format => :json
JSON.parse(response.body).count.should eq(1)
end
it "does not have something" do
hash = { "allow_sharing" => "0"}
CustomClass.stubs(:app_settings).returns(hash)
get 'method', :format => :json
JSON.parse(response.body).count.should eq(0)
end
end
I also tried it this way with a before block. Still no luck.
describe "#method" do
before do
hash = { "allow_sharing" => "1"}
CustomClass.stubs(:app_settings).returns(hash)
end
it "has something" do
get 'method', :format => :json
JSON.parse(response.body).count.should eq(1)
end
# ... etc.
try using as_null_object if thats available. so for example for all lines with stubs:
CustomClass.stubs(:app_settings).returns(hash).as_null_object

RSpec - Check condition at the time a method is called?

Right now I assert that a method is called:
Code:
def MyClass
def send_report
...
Net::SFTP.start(#host, #username, :password => #password) do |sftp|
...
end
...
end
end
Test:
it 'successfully sends file' do
Net::SFTP.
should_receive(:start).
with('bla.com', 'some_username', :password => 'some_password')
my_class.send_report
end
However, I also want to check that a given condition is true at the time Net::SFTP.start is called. How would I do something like this?
it 'successfully sends file' do
Net::SFTP.
should_receive(:start).
with('bla.com', 'some_username', :password => 'some_password').
and(<some condition> == true)
my_class.send_report
end
You could provide a block to should_receive, which will execute at the time the method is called:
it 'sends a file with the correct arguments' do
Net::SFTP.should_receive(:start) do |url, username, options|
url.should == 'bla.com'
username.should == 'some_username'
options[:password].should == 'some_password'
<some condition>.should be_true
end
my_class.send_report
end
you can use expect
it 'successfully sends file' do
Net::SFTP.
should_receive(:start).
with('bla.com', 'some_username', :password => 'some_password')
my_class.send_report
end
it 'should verify the condition also' do
expect{ Net::SFTP.start(**your params**) }to change(Thing, :status).from(0).to(1)
end
Thanks #rickyrickyrice, your answer was almost correct. The problem is that it doesn't validate the correct number of arguments passed to Net::SFTP.start. Here's what I ended up using:
it 'sends a file with the correct arguments' do
Net::SFTP.should_receive(:start).with('bla.com', 'some_username', :password => 'some_password') do
<some condition>.should be_true
end
my_class.send_report
end

Resources