I am writing tests to check if certain end-points return a status of 200.
RSpec.describe 'API -> ' do
before do
#token = get_token
end
describe 'Status of below end points should be 200 -->' do
it "/one should return a status code of 200" do
get("/api/v1/one", params: {
authentication_token: #token
})
expect(response.status).to eql(200)
end
it "/two should return a status code of 200" do
get("/api/v1/two", params: {
authentication_token: #token
})
expect(response.status).to eql(200)
end
it "/three should return a status code of 200" do
get("/api/v1/three", params: {
authentication_token: #token
})
expect(response.status).to eql(200)
end
end
end
There are many such end points and i wanted to know if there is more efficient way to write this, something like
RSpec.describe 'API -> ' do
before do
#token = get_token
#end_points = ['one', 'two', 'three', 'four', 'five']
end
describe 'Status of below end points should be 200 -->' do
#end_points.each do |end_point|
it "/#{end_point} shold returns a status code of 200" do
get("/api/v1/#{end_point}", params: {
authentication_token: #token
})
expect(response.status).to eql(200)
end
end
end
end
but this does not work and gives an error each called for nil.
Any help with this will be great, Thanks.
What you can use is a shared example.
shared_examples "returns 200 OK" do |endpoint|
let(:token) { get_token }
it "should return a status code of 200" do
get(endpoint, params: { authentication_token: token })
expect(response.status).to eql(200)
end
end
describe '..' do
include_examples 'returns 200 OK', '/api/endpoint/1'
include_examples 'returns 200 OK', '/api/endpoint/2'
end
John's answer is good, especially if you have more specs per endpoint.
As to your attempt, it was a simple mistake with scoping. Your instance variable is set on one level, but used on another. Set it on the same level.
describe 'Status of below end points should be 200 -->' do
end_points = ['one', 'two', 'three', 'four', 'five']
end_points.each do |end_point|
it "/#{end_point} shold returns a status code of 200" do
get("/api/v1/#{end_point}", params: {
authentication_token: #token
})
expect(response.status).to eql(200)
end
end
end
Related
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) }
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
I create RSpec test below. I try to call subject several times. But I cannot get expected result. I call subject three times, doesn't it? So, I expect three Book records. Does subject cannot call one time?
require 'rails_helper'
RSpec.describe Book, type: :model do
context 'some context' do
subject { Book.create }
before do
subject
end
it 'something happen' do
subject
expect { subject }.to change{ Book.count }.from(0).to(3)
end
end
end
No. let and subject are memoized (and lazy-loaded).
You can change it like this
subject { 3.times { Book.create } }
it 'something happen' do
expect { subject }.to change{ Book.count }.from(0).to(3)
end
Or if you (for whatever reason) want to call something 3 times - define a method:
subject { create_book }
def create_book
Book.create
end
before do
create_book
end
it 'something happen' do
create_book
expect { subject }.to change{ Book.count }.from(2).to(3)
end
Then it will be called 3 times: once in before block, one in it before expectation and once inside the expectation (but the change will be from 2 not from 0, because those 2 times were called before)
If you don't want to memoize a result you can also use a Proc
describe MyClass do
let(:param_1) { 'some memoized string' }
let(:described_method) { Proc.new { described_class.do_something(param_1) } }
it 'can run the method twice without memoization' do
expect(described_method.call).to have_key(:success) # => {success: true}
expect(described_method.call).to have_key(:error) # => {error: 'cant call do_something with same param twice'}
end
end
I am testing controller with rspec and I get this error
Test:
quizes_controller_spec.rb:
require 'rails_helper'
require 'spec_helper'
RSpec.describe Api::QuizzesController, :type => :controller do
before (:each) do
#instructor = create(:instructor)
end
describe "instructor_index method" do
it "returns all quizzes of the current instructor" do
sign_in #instructor
#quiz = create(:quiz)
#instructor.quizzes << #quiz
get :instructor_index
quiz_response = json(response.body)
expect(response.status).to eq(200)
expect(quiz_response[:success]).to eql(true)
expect((quiz_response[:data][:quizzes]).first[:name]).to eql(#quiz.name)
end
end
end
In index method it is rendering the quizzes of the instructor:
quizzes_controller.rb:
def instructor_index
quizzes = current_instructor.quizzes
render json: { success:true, data:{:quizzes => quizzes},info:{} }, status: 200
end
aftur runnig rspec this error appears :
Api::QuizzesController instructor_index method returns all quizzes of the current instructor
Failure/Error: quiz_response = json(response.body)
JSON::ParserError:
757: unexpected token at 'You are being redirected.'
# ./spec/rails_helper.rb:9:in json'
# ./spec/controllers/quizzes_controller_spec.rb:60:inblock (3 levels) in '
rails_helper.rb:
def json(body)
JSON.parse(body, symbolize_names: true)
end
I try to define own 'context' method in Rspec.
Have next:
module MiscSpecHelper
def its_ok
context "if everything is OK" do
yield
end
end
end
in spec file:
describe "GET index" do
its_ok do
it "has a 200 status code" do
get :index
expect(response.status).to eq(200)
end
end
end
I got:
GET index
has a 200 status code
I expect:
GET index
if everything is OK
has a 200 status code
Why does it ignore my 'context' description?
module MiscSpecHelper
def its_ok(&block)
context "if everything is OK", &block
end
end