From 4d76941d0331aea00c394384fa926d90e2c4bbea Mon Sep 17 00:00:00 2001 From: Max Melentiev Date: Fri, 25 May 2018 18:48:38 +0300 Subject: [PATCH] Support for testing bots in poller mode and non-Rails apps --- CHANGELOG.md | 4 + README.md | 35 ++++-- lib/telegram/bot/rspec.rb | 9 ++ .../bot/rspec/callback_query_helpers.rb | 7 -- lib/telegram/bot/rspec/integration.rb | 36 ------ lib/telegram/bot/rspec/integration/poller.rb | 14 +++ lib/telegram/bot/rspec/integration/rack.rb | 24 ++++ lib/telegram/bot/rspec/integration/rails.rb | 30 +++++ lib/telegram/bot/rspec/integration/shared.rb | 14 +++ .../bot/updates_controller/rspec_helpers.rb | 8 +- spec/integration_helper.rb | 2 +- spec/support/examples/integration.rb | 24 ++++ .../bot/rspec/callback_query_helpers_spec.rb | 42 +++++++ .../bot/rspec/integration/poller_spec.rb | 5 + .../bot/rspec/integration/rack_spec.rb | 20 +++ .../bot/rspec/integration/rails_spec.rb | 1 + spec/telegram/bot/rspec/integration_spec.rb | 111 ----------------- .../bot/rspec/message_helpers_spec.rb | 115 ++++++++++++++++++ 18 files changed, 331 insertions(+), 170 deletions(-) delete mode 100644 lib/telegram/bot/rspec/integration.rb create mode 100644 lib/telegram/bot/rspec/integration/poller.rb create mode 100644 lib/telegram/bot/rspec/integration/rack.rb create mode 100644 lib/telegram/bot/rspec/integration/rails.rb create mode 100644 lib/telegram/bot/rspec/integration/shared.rb create mode 100644 spec/support/examples/integration.rb create mode 100644 spec/telegram/bot/rspec/callback_query_helpers_spec.rb create mode 100644 spec/telegram/bot/rspec/integration/poller_spec.rb create mode 100644 spec/telegram/bot/rspec/integration/rack_spec.rb create mode 100644 spec/telegram/bot/rspec/integration/rails_spec.rb delete mode 100644 spec/telegram/bot/rspec/integration_spec.rb create mode 100644 spec/telegram/bot/rspec/message_helpers_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 417e14a..52ac692 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ - Changed signature `dispatch(bot, update) => dispatch(update, bot)`. - `update` helper is symbolized by default. - `build_update(type, data)` is dropped in favor of `deep_stringify(type => data)`. +- Provide support for integration testing of bots in poller mode and non-Rails apps. + __Breaking changes__: + - `telegram/bot/rspec/integration` is moved to `telegram/bot/rspec/integration/rails`. + - `:telegram_bot` rspec tag is replaced with `telegram_bot: :rails`. # 0.13.1 diff --git a/README.md b/README.md index 8b14171..11d1ee3 100644 --- a/README.md +++ b/README.md @@ -192,8 +192,9 @@ end #### Reply helpers There are helpers to respond for basic actions. They just set chat/message/query -identifiers from update. See [`ReplyHelpers`](https://github.com/telegram-bot-rb/telegram-bot/blob/master/lib/telegram/bot/updates_controller/reply_helpers.rb) module for more information. -Here are this methods signatures: +identifiers from update. See +[`ReplyHelpers`](https://github.com/telegram-bot-rb/telegram-bot/blob/master/lib/telegram/bot/updates_controller/reply_helpers.rb) +module for more information. Here are this methods signatures: ```ruby def respond_with(type, params); end @@ -421,19 +422,27 @@ tags. In RSpec < 3.4 it's require to use `include_context` explicitly. See [list of available helpers](https://github.com/telegram-bot-rb/telegram-bot/tree/master/lib/telegram/bot/rspec) for details. -Integration tests simulate webhooks from Telegram. It works for Rails applications -using bot in webhooks mode without any additional configuration, -and may require some setup in other cases. It works on top of request-spec, -so it's required to use `type: :request` or place specs in `spec/requests` directory -with RSpec's `infer_spec_type_from_file_location!`. +There are 3 types of integration tests: + +- `:rails` - for testing bot in webhooks-mode in Rails application. + It simulates webhook requests like request specs, POSTing data to controller's endpoint. +- `:rack` - for testing bot in webhooks-mode in non-Rails application. + It uses `rack-test` gem to POST requests to bot's endpoint. +- `:poller` - calls `.dispatch` directly on controller class. + +Pick the appropriate one, then require `telegram/bot/rspec/integration/#{type}` +and mark spec group with tag `telegram_bot: type`. See configuration options +for each type in `telegram/bot/rspec/integration/`. + +Here is example for Rails app: ```ruby # spec/requests/telegram_webhooks_spec.rb -require 'telegram/bot/rspec/integration' +require 'telegram/bot/rspec/integration/rails' -RSpec.describe TelegramWebhooksController, :telegram_bot do +RSpec.describe TelegramWebhooksController, telegram_bot: :rails do # for old RSpec: - # include_context 'telegram/bot/integration' + # include_context 'telegram/bot/integration/rails' # Main method is #dispatch(update). Some helpers are: # #dispatch_message(text, options = {}) @@ -466,7 +475,11 @@ end ``` There is context for testing bot controller in the way similar to Rails controller tests. -It's supposed to be low-level alternative for request specs +It's supposed to be low-level alternative for request specs. Among the differencies is +that controller tests use single controller instance for all dispatches in single exaple, +session is stubbed (does not use configured store engine), and update is not serialized +so it support mocks. This can be useful for unit testing, but should not be considered as +default way to test bot. ```ruby require 'telegram/bot/updates_controller/rspec_helpers' diff --git a/lib/telegram/bot/rspec.rb b/lib/telegram/bot/rspec.rb index 691eaad..aeba788 100644 --- a/lib/telegram/bot/rspec.rb +++ b/lib/telegram/bot/rspec.rb @@ -2,6 +2,15 @@ module Telegram module Bot module RSpec autoload :ClientMatchers, 'telegram/bot/rspec/client_matchers' + + module_function + + # Yelds a block if `include_context` is supported. + def with_include_context + ::RSpec.configure do |config| + yield(config) if config.respond_to?(:include_context) + end + end end end end diff --git a/lib/telegram/bot/rspec/callback_query_helpers.rb b/lib/telegram/bot/rspec/callback_query_helpers.rb index dda2692..64a22fe 100644 --- a/lib/telegram/bot/rspec/callback_query_helpers.rb +++ b/lib/telegram/bot/rspec/callback_query_helpers.rb @@ -3,7 +3,6 @@ require 'telegram/bot/rspec/message_helpers' # Shared helpers for testing callback query updates. RSpec.shared_context 'telegram/bot/callback_query' do - include_context 'telegram/bot/integration' include_context 'telegram/bot/message_helpers' subject { -> { dispatch callback_query: payload } } @@ -38,9 +37,3 @@ RSpec.shared_context 'telegram/bot/callback_query' do ).with(hash_including(options)) end end - -RSpec.configure do |config| - if config.respond_to?(:include_context) - config.include_context 'telegram/bot/callback_query', :telegram_bot, :callback_query - end -end diff --git a/lib/telegram/bot/rspec/integration.rb b/lib/telegram/bot/rspec/integration.rb deleted file mode 100644 index 63fbaac..0000000 --- a/lib/telegram/bot/rspec/integration.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'telegram/bot/rspec/message_helpers' -require 'telegram/bot/rspec/callback_query_helpers' - -RSpec.shared_context 'telegram/bot/integration' do - include Telegram::Bot::RSpec::ClientMatchers - include_context 'telegram/bot/message_helpers' - - let(:bot) { Telegram.bot } - let(:controller_path) do - route_name = Telegram::Bot::RoutesHelper.route_name_for_bot(bot) - Rails.application.routes.url_helpers.public_send("#{route_name}_path") - end - let(:request_headers) do - { - 'ACCEPT' => 'application/json', - 'Content-Type' => 'application/json', - } - end - let(:clear_session?) { described_class.respond_to?(:session_store) } - before { described_class.session_store.try!(:clear) if clear_session? } - - # Process update. - def dispatch(update) - if ActionPack::VERSION::MAJOR >= 5 - post(controller_path, params: update.to_json, headers: request_headers) - else - post(controller_path, update.to_json, request_headers) - end - end -end - -RSpec.configure do |config| - if config.respond_to?(:include_context) - config.include_context 'telegram/bot/integration', :telegram_bot - end -end diff --git a/lib/telegram/bot/rspec/integration/poller.rb b/lib/telegram/bot/rspec/integration/poller.rb new file mode 100644 index 0000000..54f7b8d --- /dev/null +++ b/lib/telegram/bot/rspec/integration/poller.rb @@ -0,0 +1,14 @@ +require 'telegram/bot/rspec/integration/shared' + +RSpec.shared_context 'telegram/bot/integration/poller' do + include_context 'telegram/bot/integration/shared' + let(:controller_class) { described_class } + + def dispatch(update) + controller_class.dispatch(bot, update.as_json) + end +end + +Telegram::Bot::RSpec.with_include_context do |config| + config.include_context 'telegram/bot/integration/poller', telegram_bot: :poller +end diff --git a/lib/telegram/bot/rspec/integration/rack.rb b/lib/telegram/bot/rspec/integration/rack.rb new file mode 100644 index 0000000..520d90c --- /dev/null +++ b/lib/telegram/bot/rspec/integration/rack.rb @@ -0,0 +1,24 @@ +require 'telegram/bot/rspec/integration/shared' +require 'rack/test' + +RSpec.shared_context 'telegram/bot/integration/rack' do + include_context 'telegram/bot/integration/shared' + include Rack::Test::Methods + + let(:request_path) { raise '`let(:request_path) { path to bot }` is required' } + let(:app) { raise '`let(:app) { your rack app here }` is required' } + let(:request_headers) do + { + 'ACCEPT' => 'application/json', + 'CONTENT_TYPE' => 'application/json', + } + end + + def dispatch(update) + post request_path, update.to_json, request_headers + end +end + +Telegram::Bot::RSpec.with_include_context do |config| + config.include_context 'telegram/bot/integration/rack', telegram_bot: :rack +end diff --git a/lib/telegram/bot/rspec/integration/rails.rb b/lib/telegram/bot/rspec/integration/rails.rb new file mode 100644 index 0000000..d51c97d --- /dev/null +++ b/lib/telegram/bot/rspec/integration/rails.rb @@ -0,0 +1,30 @@ +require 'telegram/bot/rspec/integration/shared' + +# +RSpec.shared_context 'telegram/bot/integration/rails', type: :request do + include_context 'telegram/bot/integration/shared' + + let(:controller_path) do + route_name = Telegram::Bot::RoutesHelper.route_name_for_bot(bot) + Rails.application.routes.url_helpers.public_send("#{route_name}_path") + end + let(:request_headers) do + { + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + } + end + + def dispatch(update) + if ActionPack::VERSION::MAJOR >= 5 + post(controller_path, params: update.to_json, headers: request_headers) + else + post(controller_path, update.to_json, request_headers) + end + end +end + +Telegram::Bot::RSpec.with_include_context do |config| + config.include_context 'telegram/bot/integration/rails', :telegram_bot + config.include_context 'telegram/bot/integration/rails', telegram_bot: :rails +end diff --git a/lib/telegram/bot/rspec/integration/shared.rb b/lib/telegram/bot/rspec/integration/shared.rb new file mode 100644 index 0000000..73f3087 --- /dev/null +++ b/lib/telegram/bot/rspec/integration/shared.rb @@ -0,0 +1,14 @@ +require 'active_support/json' +require 'telegram/bot' +require 'telegram/bot/rspec/message_helpers' +require 'telegram/bot/rspec/callback_query_helpers' + +RSpec.shared_context 'telegram/bot/integration/shared' do + include Telegram::Bot::RSpec::ClientMatchers + include_context 'telegram/bot/message_helpers' + include_context 'telegram/bot/callback_query', :callback_query + + let(:bot) { Telegram.bot } + let(:clear_session?) { described_class.respond_to?(:session_store) } + before { described_class.session_store.try!(:clear) if clear_session? } +end diff --git a/lib/telegram/bot/updates_controller/rspec_helpers.rb b/lib/telegram/bot/updates_controller/rspec_helpers.rb index 33467e8..df3d25f 100644 --- a/lib/telegram/bot/updates_controller/rspec_helpers.rb +++ b/lib/telegram/bot/updates_controller/rspec_helpers.rb @@ -1,9 +1,11 @@ require 'telegram/bot/updates_controller/testing' require 'telegram/bot/rspec/message_helpers' +require 'telegram/bot/rspec/callback_query_helpers' RSpec.shared_context 'telegram/bot/updates_controller' do include Telegram::Bot::RSpec::ClientMatchers include_context 'telegram/bot/message_helpers' + include_context 'telegram/bot/callback_query', :callback_query let(:controller_class) { described_class } let(:controller) do @@ -34,8 +36,6 @@ RSpec.shared_context 'telegram/bot/updates_controller' do end end -RSpec.configure do |config| - if config.respond_to?(:include_context) - config.include_context 'telegram/bot/updates_controller', type: :telegram_bot_controller - end +Telegram::Bot::RSpec.with_include_context do |config| + config.include_context 'telegram/bot/updates_controller', type: :telegram_bot_controller end diff --git a/spec/integration_helper.rb b/spec/integration_helper.rb index 648d835..e6eef97 100644 --- a/spec/integration_helper.rb +++ b/spec/integration_helper.rb @@ -1,4 +1,4 @@ -require 'telegram/bot/rspec/integration' +require 'telegram/bot/rspec/integration/rails' require 'action_controller' require 'action_dispatch' diff --git a/spec/support/examples/integration.rb b/spec/support/examples/integration.rb new file mode 100644 index 0000000..00ee1ac --- /dev/null +++ b/spec/support/examples/integration.rb @@ -0,0 +1,24 @@ +RSpec.shared_examples 'shared integration examples' do + let(:bot) { Telegram::Bot::ClientStub.new('token') } + let(:controller_class) do + Class.new(Telegram::Bot::UpdatesController) do + def start(data = nil, *) + respond_with :message, text: "Hi #{data}" + end + + def callback_query(data, *) + answer_callback_query "pong: #{data}" + end + end + end + + describe '#start' do + subject { -> { dispatch_command(:start, :test_data) } } + it { should respond_with_message('Hi test_data') } + end + + describe '#callback_query', :callback_query do + let(:data) { :test_data } + it { should answer_callback_query "pong: #{data}" } + end +end diff --git a/spec/telegram/bot/rspec/callback_query_helpers_spec.rb b/spec/telegram/bot/rspec/callback_query_helpers_spec.rb new file mode 100644 index 0000000..f32397e --- /dev/null +++ b/spec/telegram/bot/rspec/callback_query_helpers_spec.rb @@ -0,0 +1,42 @@ +require 'telegram/bot/rspec/integration/poller' + +RSpec.describe 'Integration spec helpers', telegram_bot: :poller do + let(:bot) { Telegram::Bot::ClientStub.new('token') } + let(:controller_class) do + Class.new(Telegram::Bot::UpdatesController) do + include Telegram::Bot::UpdatesController::CallbackQueryContext + + def callback_query(data = nil, *) + answer_callback_query "data: #{data}" + end + + def context_callback_query(data = nil, *) + answer_callback_query "data: #{data}", extra: :param + end + + def answer_and_edit_callback_query(data = nil, *) + answer_callback_query "data: #{data}" + edit_message :text, text: 'edited-text', extra: :param + end + end + end + + describe '#callback_query', :callback_query do + let(:data) { 'unknown:command' } + it { should answer_callback_query("data: #{data}") } + end + + describe '#context_callback_query', :callback_query do + let(:data) { 'context:test:payload' } + it { should answer_callback_query('data: test:payload', extra: :param) } + it { should_not edit_current_message(:text) } + end + + describe '#answer_and_edit_callback_query', :callback_query do + let(:data) { 'answer_and_edit:test:payload' } + it do + should answer_callback_query(/test:payload/). + and edit_current_message(:text, text: /edited/, extra: :param) + end + end +end diff --git a/spec/telegram/bot/rspec/integration/poller_spec.rb b/spec/telegram/bot/rspec/integration/poller_spec.rb new file mode 100644 index 0000000..27608aa --- /dev/null +++ b/spec/telegram/bot/rspec/integration/poller_spec.rb @@ -0,0 +1,5 @@ +require 'telegram/bot/rspec/integration/poller' + +RSpec.describe 'Poller integration spec', telegram_bot: :poller do + include_examples 'shared integration examples' +end diff --git a/spec/telegram/bot/rspec/integration/rack_spec.rb b/spec/telegram/bot/rspec/integration/rack_spec.rb new file mode 100644 index 0000000..89c3622 --- /dev/null +++ b/spec/telegram/bot/rspec/integration/rack_spec.rb @@ -0,0 +1,20 @@ +require 'telegram/bot/rspec/integration/rack' + +RSpec.describe 'Rack integration spec', telegram_bot: :rack do + include_examples 'shared integration examples' + let(:request_path) { '/bot' } + let(:app) do + path = request_path + bot_app = Telegram::Bot::Middleware.new(bot, controller_class) + app = Rack::Builder.new do + map(path) { run bot_app } + run ->(env) { raise "Route is not mapped: #{env['PATH_INFO']}" } + end + if ActionPack::VERSION::MAJOR >= 5 + app + else + require 'action_dispatch/middleware/params_parser' + ActionDispatch::ParamsParser.new(app) + end + end +end diff --git a/spec/telegram/bot/rspec/integration/rails_spec.rb b/spec/telegram/bot/rspec/integration/rails_spec.rb new file mode 100644 index 0000000..05c498e --- /dev/null +++ b/spec/telegram/bot/rspec/integration/rails_spec.rb @@ -0,0 +1 @@ +# Tested in spec/integration diff --git a/spec/telegram/bot/rspec/integration_spec.rb b/spec/telegram/bot/rspec/integration_spec.rb deleted file mode 100644 index 758fee5..0000000 --- a/spec/telegram/bot/rspec/integration_spec.rb +++ /dev/null @@ -1,111 +0,0 @@ -require 'telegram/bot/rspec/integration' -require 'action_controller' -require 'action_dispatch' - -RSpec.describe 'Integrations helper', :telegram_bot do - include ActionDispatch::Integration::Runner - def reset_template_assertion - end - - let(:app) do - app = Telegram::Bot::Middleware.new(bot, controller) - if ActionPack::VERSION::MAJOR >= 5 - app - else - require 'action_dispatch/middleware/params_parser' - ActionDispatch::ParamsParser.new(app) - end - end - let(:bot) { Telegram::Bot::ClientStub.new('token') } - let(:controller_path) { '/' } - let(:controller) do - Class.new(Telegram::Bot::UpdatesController) do - def start(*args) - respond_with :message, text: "Start: #{args.inspect}, option: #{payload[:option]}" - end - end - end - - describe '#default_message_options' do - subject { default_message_options } - it { should eq from: {id: from_id}, chat: {id: chat_id} } - end - - describe '#dispatch' do - subject { -> { dispatch message: {text: '/start', **default_message_options} } } - it { should respond_with_message 'Start: [], option: ' } - end - - describe '#dispatch_message' do - subject { -> { dispatch_message "/start #{args.join ' '}", options } } - let(:args) { %w[asd qwe] } - let(:options) { {} } - it { should respond_with_message "Start: #{args.inspect}, option: " } - - context 'with options' do - let(:options) { {option: 1} } - it { should respond_with_message "Start: #{args.inspect}, option: 1" } - - context 'and chat_id is not set' do - let(:options) { super().merge(chat: nil) } - it { should raise_error(/chat is not present/) } - end - end - end - - describe '#dispatch_command' do - subject { -> { dispatch_command :start, *args } } - let(:args) { [] } - it { should respond_with_message "Start: #{args.inspect}, option: " } - - context 'with args' do - let(:args) { %w[asd qwe] } - it { should respond_with_message "Start: #{args.inspect}, option: " } - end - - context 'with options' do - let(:args) { ['asd', 'qwe', option: 1] } - it { should respond_with_message "Start: #{args[0...-1].inspect}, option: 1" } - end - end - - describe 'callback queries', :callback_query do - let(:controller) do - Class.new(Telegram::Bot::UpdatesController) do - include Telegram::Bot::UpdatesController::CallbackQueryContext - - def callback_query(data = nil, *) - answer_callback_query "data: #{data}" - end - - def context_callback_query(data = nil, *) - answer_callback_query "data: #{data}", extra: :param - end - - def answer_and_edit_callback_query(data = nil, *) - answer_callback_query "data: #{data}" - edit_message :text, text: 'edited-text', extra: :param - end - end - end - - describe '#callback_query' do - let(:data) { 'unknown:command' } - it { should answer_callback_query("data: #{data}") } - end - - describe '#context_callback_query' do - let(:data) { 'context:test:payload' } - it { should answer_callback_query('data: test:payload', extra: :param) } - it { should_not edit_current_message(:text) } - end - - describe '#answer_and_edit_callback_query' do - let(:data) { 'answer_and_edit:test:payload' } - it do - should answer_callback_query(/test:payload/). - and edit_current_message(:text, text: /edited/, extra: :param) - end - end - end -end diff --git a/spec/telegram/bot/rspec/message_helpers_spec.rb b/spec/telegram/bot/rspec/message_helpers_spec.rb new file mode 100644 index 0000000..372783d --- /dev/null +++ b/spec/telegram/bot/rspec/message_helpers_spec.rb @@ -0,0 +1,115 @@ +require 'telegram/bot/rspec/integration/poller' + +RSpec.describe 'Integration: message helpers', telegram_bot: :poller do + describe '#default_message_options' do + subject { default_message_options } + it { should eq from: {id: from_id}, chat: {id: chat_id} } + end + + describe '#dispatch_message' do + subject { -> { dispatch_message text, options } } + let(:text) { '/start asd qwe' } + let(:options) { {} } + let(:result) { double(:result) } + + it 'invokes dispatch' do + expect(self).to receive(:dispatch).with( + message: hash_including(default_message_options.merge( + text: text, + )), + ) { result } + expect(subject.call).to eq result + end + + context 'with options' do + let(:options) { {option: 1} } + it 'invokes dispatch' do + expect(self).to receive(:dispatch).with( + message: hash_including(default_message_options.merge( + text: text, + ).merge(options)), + ) { result } + expect(subject.call).to eq result + end + end + end + + describe '#dispatch_command' do + subject { -> { dispatch_command :start, *args } } + let(:args) { [] } + let(:result) { double(:result) } + + it 'invokes dispatch' do + expect(self).to receive(:dispatch).with( + message: hash_including(default_message_options.merge( + text: '/start', + )), + ) { result } + expect(subject.call).to eq result + end + + context 'with args & options' do + let(:args) { [*params, options] } + let(:params) { %w[qwe asd] } + let(:options) { {option: 1} } + it 'invokes dispatch' do + expect(self).to receive(:dispatch).with( + message: hash_including(default_message_options.merge( + text: "/start #{params.join(' ')}", + ).merge(options)), + ) { result } + expect(subject.call).to eq result + end + end + end +end + +# Old specs +RSpec.describe 'Integration: message helpers', telegram_bot: :poller do + let(:bot) { Telegram::Bot::ClientStub.new('token') } + let(:controller_class) do + Class.new(Telegram::Bot::UpdatesController) do + def start(*args) + respond_with :message, text: "Start: #{args.inspect}, option: #{payload['option']}" + end + end + end + + describe '#default_message_options' do + subject { default_message_options } + it { should eq from: {id: from_id}, chat: {id: chat_id} } + end + + describe '#dispatch_message' do + subject { -> { dispatch_message "/start #{args.join ' '}", options } } + let(:args) { %w[asd qwe] } + let(:options) { {} } + it { should respond_with_message "Start: #{args.inspect}, option: " } + + context 'with options' do + let(:options) { {option: 1} } + it { should respond_with_message "Start: #{args.inspect}, option: 1" } + + context 'and chat_id is not set' do + let(:options) { super().merge(chat: nil) } + it { should raise_error(/chat is not present/) } + end + end + end + + describe '#dispatch_command' do + subject { -> { dispatch_command :start, *args } } + let(:args) { [] } + it { should respond_with_message "Start: #{args.inspect}, option: " } + + context 'with args' do + let(:args) { %w[asd qwe] } + it { should respond_with_message "Start: #{args.inspect}, option: " } + end + + context 'with options' do + let(:args) { ['asd', 'qwe', option: 1] } + it { should respond_with_message "Start: #{args[0...-1].inspect}, option: 1" } + end + end +end