From eeb2569bd78cb7e18c47c05757d8c461a16944cb Mon Sep 17 00:00:00 2001 From: Max Melentiev Date: Mon, 28 May 2018 19:41:42 +0300 Subject: [PATCH 1/7] Remve deprecated telegram_webhooks --- lib/telegram/bot.rb | 4 +- lib/telegram/bot/routes_helper.rb | 27 ------ spec/telegram/bot/routes_helper_spec.rb | 104 ------------------------ 3 files changed, 2 insertions(+), 133 deletions(-) diff --git a/lib/telegram/bot.rb b/lib/telegram/bot.rb index fdb15fe..ce860e3 100644 --- a/lib/telegram/bot.rb +++ b/lib/telegram/bot.rb @@ -14,10 +14,10 @@ module Telegram module_function - def deprecation_0_14 + def deprecation_0_15 @deprecation ||= begin require 'active_support/deprecation' - ActiveSupport::Deprecation.new('0.14', 'Telegram::Bot') + ActiveSupport::Deprecation.new('0.15', 'Telegram::Bot') end end diff --git a/lib/telegram/bot/routes_helper.rb b/lib/telegram/bot/routes_helper.rb index 9862c83..4c386ef 100644 --- a/lib/telegram/bot/routes_helper.rb +++ b/lib/telegram/bot/routes_helper.rb @@ -24,33 +24,6 @@ module Telegram end end - # # Create routes for all Telegram.bots to use same controller: - # telegram_webhooks TelegramController - # - # # Or pass custom bots usin any of supported config options: - # telegram_webhooks TelegramController, [ - # bot, - # {token: token, username: username}, - # other_bot_token, - # ] - def telegram_webhooks(controllers, bots = nil, **options) - Bot.deprecation_0_14.deprecation_warning(:telegram_webhooks, <<-TXT.strip_heredoc) - It brings unnecessary complexity and encourages writeng less readable code. - Please use telegram_webhook method instead. - It's signature `telegram_webhook(controller, bot = :default, **options)`. - Multiple-bot environments now requires calling this method in a loop - or using statement for each bot. - TXT - unless controllers.is_a?(Hash) - bots = bots ? Array.wrap(bots) : Telegram.bots.values - controllers = Hash[bots.map { |x| [x, controllers] }] - end - controllers.each do |bot, controller| - controller, bot_options = controller if controller.is_a?(Array) - telegram_webhook(controller, bot, options.merge(bot_options || {})) - end - end - # Define route which processes requests using given controller and bot. # # telegram_webhook TelegramController, bot diff --git a/spec/telegram/bot/routes_helper_spec.rb b/spec/telegram/bot/routes_helper_spec.rb index dbfa1bc..0a92af2 100644 --- a/spec/telegram/bot/routes_helper_spec.rb +++ b/spec/telegram/bot/routes_helper_spec.rb @@ -95,108 +95,4 @@ RSpec.describe Telegram::Bot::RoutesHelper do end end end - - describe '#telegram_webhooks' do - subject { mapper.telegram_webhooks(*input) } - let(:mapper) { double(:mapper).tap { |x| x.extend described_class } } - let(:bots) { {default: bot, other: other_bot} } - let(:controller) { double(:controller, name: :controller) } - let(:other_controller) { double(:other_controller, name: :other_controller) } - before { allow(Telegram).to receive(:bots) { bots } } - around { |ex| Telegram::Bot.deprecation_0_14.silence { ex.run } } - - def assert_routes(*expected) # rubocop:disable AbcSize - expected.each do |(bot, controller, route_name, options)| - expected_path = options.delete(:path) || "telegram/#{bot.token}" - expect(mapper).to receive(:post) do |path, params| - expect(path).to eq expected_path - middleware = params[:to] - expect(middleware.controller).to eq(controller) - expect(middleware.bot.token).to eq(bot.token) - expect(middleware.bot.username).to eq(bot.username) - expect(params[:as]).to eq route_name - expect(params).to include(options) if options - end - end - subject - end - - context 'when called with controller' do - let(:input) { [controller, option: :val] } - - it 'creates routes for every bot and this controller' do - assert_routes [bot, controller, 'default_telegram_webhook', option: :val], - [other_bot, controller, 'other_telegram_webhook', option: :val] - end - - context 'and bot does not have configured token' do - let(:bot) { create_bot(nil) } - it 'creates routes for every bot and this controller' do - assert_routes [bot, controller, 'default_telegram_webhook', option: :val], - [other_bot, controller, 'other_telegram_webhook', option: :val] - end - end - - context 'and bot has colon in token' do - let(:bot) { create_bot('some:token') } - it 'replaces colon with underscore' do - assert_routes [ - bot, - controller, - 'default_telegram_webhook', - option: :val, - path: 'telegram/some_token', - ], [other_bot, controller, 'other_telegram_webhook', option: :val] - end - end - end - - context 'when called with hash' do - let(:input) do - [ - { - bot => controller, - 'custom_token' => [other_controller, as: :custom_route, option: :other_val], - other: controller, - }, - option: :val, - ] - end - - it 'creates routes for every bot and its controller' do - assert_routes [bot, controller, 'default_telegram_webhook', option: :val], - [ - create_bot('custom_token'), - other_controller, - :custom_route, - option: :other_val, - ], - [Telegram.bots[:other], controller, 'other_telegram_webhook', option: :val] - end - end - - context 'when called with controller and smth castable to bot' do - let(:input) do - [ - controller, - ['custom_token', token: bot.token, username: 'new_name'], - option: :val, - ] - end - - it 'creates routes for every created bot and controller' do - assert_routes [ - create_bot('custom_token'), - controller, - 'telegram_webhook', - option: :val, - ], [ - create_bot(bot.token, 'new_name'), - controller, - 'telegram_webhook', - option: :val, - ] - end - end - end end From 673986ca7ef5c8e2ef27cbbccbd1b2e88e9d2d6d Mon Sep 17 00:00:00 2001 From: Max Melentiev Date: Mon, 21 May 2018 12:19:44 +0300 Subject: [PATCH 2/7] Make integration & controller specs consistent --- CHANGELOG.md | 8 +++ README.md | 60 +++++++++++------- .../bot/rspec/callback_query_helpers.rb | 46 ++++++++++++++ lib/telegram/bot/rspec/integration.rb | 63 +++---------------- lib/telegram/bot/rspec/message_helpers.rb | 26 ++++++++ .../bot/updates_controller/rspec_helpers.rb | 34 ++++------ spec/telegram/bot/middleware_spec.rb | 1 - .../callback_query_context_spec.rb | 2 +- .../instrumentation_spec.rb | 8 +-- .../updates_controller/reply_helpers_spec.rb | 16 +++-- .../bot/updates_controller/session_spec.rb | 2 +- spec/telegram/bot/updates_controller_spec.rb | 8 ++- 12 files changed, 154 insertions(+), 120 deletions(-) create mode 100644 lib/telegram/bot/rspec/callback_query_helpers.rb create mode 100644 lib/telegram/bot/rspec/message_helpers.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 954ccb2..417e14a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# Unreleased + +- Make integration & controller specs consistent. + __Breaking changes__ for controller specs: + - 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)`. + # 0.13.1 - Extracted typed response mappings to telegram-bot-types gem. diff --git a/README.md b/README.md index e4bc808..8b14171 100644 --- a/README.md +++ b/README.md @@ -416,47 +416,65 @@ RSpec.configure do |config| end ``` -There are integration and controller contexts for RSpec and some built-in matchers: +RSpec contexts and helpers are included automatically for groups and examples with matching +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!`. ```ruby # spec/requests/telegram_webhooks_spec.rb require 'telegram/bot/rspec/integration' RSpec.describe TelegramWebhooksController, :telegram_bot do - # for old rspec add: + # for old RSpec: # include_context 'telegram/bot/integration' + # Main method is #dispatch(update). Some helpers are: + # #dispatch_message(text, options = {}) + # #dispatch_command(cmd, *args) + + # Available matchers can be found in Telegram::Bot::RSpec::ClientMatchers. + it 'shows usage of basic matchers' + # The most basic one is #make_telegram_request(bot, endpoint, params_matcher) + expect { dispatch_command(:start) }. + to make_telegram_request(bot, :sendMessage, hash_including(text: 'msg text')) + # There are some shortcuts for ba + expect { send_message('Hi') }.to send_telegram_message(bot, /msg regexp/, some: :option) + end + describe '#start' do - subject { -> { dispatch_command :start } } + subject { -> { dispatch_command :start } }sic actions. + # Using built in matcher for `respond_to`: it { should respond_with_message 'Hi there!' } end - # There is context for callback queries with related matchers. + # There is context for callback queries with related matchers, + # use :callback_query tag to include it. describe '#hey_callback_query', :callback_query do let(:data) { "hey:#{name}" } let(:name) { 'Joe' } it { should answer_callback_query('Hey Joe') } it { should edit_current_message :text, text: 'Done' } + end end - -# For controller specs use -require 'telegram/bot/updates_controller/rspec_helpers' -RSpec.describe TelegramWebhooksController, type: :telegram_bot_controller do - # for old rspec add: - # include_context 'telegram/bot/updates_controller' -end - -# Matchers are available for custom specs: -include Telegram::Bot::RSpec::ClientMatchers - -expect(&process_update).to send_telegram_message(bot, /msg regexp/, some: :option) -expect(&process_update). - to make_telegram_request(bot, :sendMessage, hash_including(text: 'msg text')) ``` -Place integration tests inside `spec/requests` -when using RSpec's `infer_spec_type_from_file_location!`, -or just add `type: :request` to `describe`. +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 + +```ruby +require 'telegram/bot/updates_controller/rspec_helpers' +RSpec.describe TelegramWebhooksController, type: :telegram_bot_controller do + # for old RSpec: + # include_context 'telegram/bot/updates_controller' +end +``` See sample app for more examples. diff --git a/lib/telegram/bot/rspec/callback_query_helpers.rb b/lib/telegram/bot/rspec/callback_query_helpers.rb new file mode 100644 index 0000000..dda2692 --- /dev/null +++ b/lib/telegram/bot/rspec/callback_query_helpers.rb @@ -0,0 +1,46 @@ +require 'telegram/bot/rspec' +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 } } + let(:payload) { {id: callback_query_id, from: from, message: message, data: data} } + let(:callback_query_id) { 11 } + let(:message_id) { 22 } + let(:message) { {message_id: message_id, chat: chat, text: 'message text'} } + let(:data) { raise '`let(:data) { "callback query data here" }` is required' } + + # Matcher to check that origin message got edited. + def edit_current_message(type, options = {}) + description = 'edit current message' + options = options.merge( + message_id: message[:message_id], + chat_id: chat_id, + ) + Telegram::Bot::RSpec::ClientMatchers::MakeTelegramRequest.new( + bot, :"editMessage#{type.to_s.camelize}", description: description + ).with(hash_including(options)) + end + + # Matcher to check that callback query is answered. + def answer_callback_query(text = Regexp.new(''), options = {}) + description = "answer callback query with #{text.inspect}" + text = a_string_matching(text) if text.is_a?(Regexp) + options = options.merge( + callback_query_id: payload[:id], + text: text, + ) + Telegram::Bot::RSpec::ClientMatchers::MakeTelegramRequest.new( + bot, :answerCallbackQuery, description: description + ).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 index a6f9eda..63fbaac 100644 --- a/lib/telegram/bot/rspec/integration.rb +++ b/lib/telegram/bot/rspec/integration.rb @@ -1,10 +1,11 @@ +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(:default_message_options) { {from: from, chat: chat} } - let(:from) { {id: from_id} } - let(:from_id) { 123 } - let(:chat) { {id: chat_id} } - let(:chat_id) { 456 } let(:controller_path) do route_name = Telegram::Bot::RoutesHelper.route_name_for_bot(bot) Rails.application.routes.url_helpers.public_send("#{route_name}_path") @@ -18,8 +19,7 @@ RSpec.shared_context 'telegram/bot/integration' do let(:clear_session?) { described_class.respond_to?(:session_store) } before { described_class.session_store.try!(:clear) if clear_session? } - include Telegram::Bot::RSpec::ClientMatchers - + # Process update. def dispatch(update) if ActionPack::VERSION::MAJOR >= 5 post(controller_path, params: update.to_json, headers: request_headers) @@ -27,59 +27,10 @@ RSpec.shared_context 'telegram/bot/integration' do post(controller_path, update.to_json, request_headers) end end - - def dispatch_message(text, options = {}) - dispatch message: default_message_options.merge(options).merge(text: text) - end - - def dispatch_command(*args) - options = args.last.is_a?(Hash) ? args.pop : {} - dispatch_message("/#{args.join ' '}", options) - end - - # Matcher to check response. Make sure to define `let(:chat_id)`. - def respond_with_message(expected = Regexp.new('')) - raise 'Define chat_id to use respond_with_message' unless defined?(chat_id) - send_telegram_message(bot, expected, chat_id: chat_id) - end -end - -RSpec.shared_context 'telegram/bot/callback_query', callback_query: true do - include_context 'telegram/bot/integration' - - subject { -> { dispatch callback_query: payload } } - let(:payload) { {id: 11, from: from, message: message, data: data} } - let(:message) { {message_id: 22, chat: chat, text: 'message text'} } - - # Matcher to check that origin message got edited. - def edit_current_message(type, options = {}) - description = 'edit current message' - options = options.merge( - message_id: message[:message_id], - chat_id: chat_id, - ) - Telegram::Bot::RSpec::ClientMatchers::MakeTelegramRequest.new( - bot, :"editMessage#{type.to_s.camelize}", description: description - ).with(hash_including(options)) - end - - # Matcher to check that callback query is answered. - def answer_callback_query(text = Regexp.new(''), options = {}) - description = "answer callback query with #{text.inspect}" - text = a_string_matching(text) if text.is_a?(Regexp) - options = options.merge( - callback_query_id: payload[:id], - text: text, - ) - Telegram::Bot::RSpec::ClientMatchers::MakeTelegramRequest.new( - bot, :answerCallbackQuery, description: description - ).with(hash_including(options)) - end end RSpec.configure do |config| if config.respond_to?(:include_context) config.include_context 'telegram/bot/integration', :telegram_bot - config.include_context 'telegram/bot/callback_query', :telegram_bot, :callback_query end end diff --git a/lib/telegram/bot/rspec/message_helpers.rb b/lib/telegram/bot/rspec/message_helpers.rb new file mode 100644 index 0000000..6585f0c --- /dev/null +++ b/lib/telegram/bot/rspec/message_helpers.rb @@ -0,0 +1,26 @@ +# Shared helpers for testing message updates. +RSpec.shared_context 'telegram/bot/message_helpers' do + let(:default_message_options) { {from: from, chat: chat} } + let(:from) { {id: from_id} } + let(:from_id) { 123 } + let(:chat) { {id: chat_id} } + let(:chat_id) { 456 } + + # Shortcut for dispatching messages with default params. + def dispatch_message(text, options = {}) + dispatch message: default_message_options.merge(options).merge(text: text) + end + + # Dispatch command message. + def dispatch_command(cmd, *args) + options = args.last.is_a?(Hash) ? args.pop : {} + args.unshift("/#{cmd}") + dispatch_message(args.join(' '), options) + end + + # Matcher to check response. Make sure to define `let(:chat_id)`. + def respond_with_message(expected = Regexp.new('')) + raise 'Define chat_id to use respond_with_message' unless defined?(chat_id) + send_telegram_message(bot, expected, chat_id: chat_id) + end +end diff --git a/lib/telegram/bot/updates_controller/rspec_helpers.rb b/lib/telegram/bot/updates_controller/rspec_helpers.rb index 1145a61..33467e8 100644 --- a/lib/telegram/bot/updates_controller/rspec_helpers.rb +++ b/lib/telegram/bot/updates_controller/rspec_helpers.rb @@ -1,37 +1,30 @@ require 'telegram/bot/updates_controller/testing' +require 'telegram/bot/rspec/message_helpers' RSpec.shared_context 'telegram/bot/updates_controller' do + include Telegram::Bot::RSpec::ClientMatchers + include_context 'telegram/bot/message_helpers' + let(:controller_class) { described_class } let(:controller) do - controller_class.new(bot, update).tap do |x| + controller_class.new(*controller_args).tap do |x| x.extend Telegram::Bot::UpdatesController::Testing end end - let(:update) { build_update(payload_type, payload) } + let(:controller_args) { [bot, deep_stringify(update)] } + let(:update) { {payload_type => payload} } let(:payload_type) { :some_type } let(:payload) { double(:payload) } let(:bot) { Telegram::Bot::ClientStub.new(bot_name) } let(:bot_name) { 'bot' } let(:session) { controller.send(:session) } - let(:from_id) { 123 } - let(:chat_id) { 456 } - let(:default_message_options) { {from: {id: from_id}, chat: {id: chat_id}} } - include Telegram::Bot::RSpec::ClientMatchers - - def dispatch(bot = self.bot, update = self.update) - controller.dispatch_again(bot, update) - end - - def dispatch_message(text, options = {}) - update = build_update :message, default_message_options.merge(options).merge(text: text) - dispatch bot, update - end - - def build_update(type, content) - deep_stringify type => content + # Process update. + def dispatch(update = self.update, bot = self.bot) + controller.dispatch_again(bot, deep_stringify(update)) end + # Same as `.as_json` but mocks-friendly. def deep_stringify(input) case input when Array then input.map(&method(__callee__)) @@ -39,11 +32,6 @@ RSpec.shared_context 'telegram/bot/updates_controller' do else input end end - - # Matcher to check response. Make sure to define `let(:chat_id)`. - def respond_with_message(expected) - send_telegram_message(bot, expected, chat_id: chat_id) - end end RSpec.configure do |config| diff --git a/spec/telegram/bot/middleware_spec.rb b/spec/telegram/bot/middleware_spec.rb index dff57d6..e5d48b4 100644 --- a/spec/telegram/bot/middleware_spec.rb +++ b/spec/telegram/bot/middleware_spec.rb @@ -7,7 +7,6 @@ RSpec.describe Telegram::Bot::Middleware do describe '#call' do subject { instance.call(env) } - let(:env) { {'action_dispatch.request.request_parameters' => json_body} } let(:update) { {'message' => {'id' => 1}} } let(:env) do Rack::MockRequest.env_for('/', diff --git a/spec/telegram/bot/updates_controller/callback_query_context_spec.rb b/spec/telegram/bot/updates_controller/callback_query_context_spec.rb index c4e5588..757e939 100644 --- a/spec/telegram/bot/updates_controller/callback_query_context_spec.rb +++ b/spec/telegram/bot/updates_controller/callback_query_context_spec.rb @@ -27,7 +27,7 @@ RSpec.describe Telegram::Bot::UpdatesController::CallbackQueryContext do describe '#dispatch' do subject { -> { dispatch } } let(:payload_type) { :callback_query } - let(:payload) { {'data' => data} } + let(:payload) { {data: data} } let(:data) { text } let(:text) { 'asd qwe zxc' } diff --git a/spec/telegram/bot/updates_controller/instrumentation_spec.rb b/spec/telegram/bot/updates_controller/instrumentation_spec.rb index e7f588b..c8a7258 100644 --- a/spec/telegram/bot/updates_controller/instrumentation_spec.rb +++ b/spec/telegram/bot/updates_controller/instrumentation_spec.rb @@ -2,9 +2,7 @@ RSpec.describe Telegram::Bot::UpdatesController::Instrumentation do include_context 'telegram/bot/updates_controller' subject { -> { dispatch } } - let(:update) do - build_update :message, default_message_options.merge(text: '/start') - end + let(:update) { {message: default_message_options.merge(text: '/start')} } let(:controller_class) do Class.new(Telegram::Bot::UpdatesController) do @@ -36,11 +34,11 @@ RSpec.describe Telegram::Bot::UpdatesController::Instrumentation do action = action_name(:start_processing) expect(events[action].size).to eq(1) - expect(events[action][0].last).to include(update: update) + expect(events[action][0].last).to include(update: deep_stringify(update)) action = action_name(:process_action) expect(events[action].size).to eq(1) - expect(events[action][0].last).to include(update: update) + expect(events[action][0].last).to include(update: deep_stringify(update)) end end diff --git a/spec/telegram/bot/updates_controller/reply_helpers_spec.rb b/spec/telegram/bot/updates_controller/reply_helpers_spec.rb index d5f2e0c..e5ad3b8 100644 --- a/spec/telegram/bot/updates_controller/reply_helpers_spec.rb +++ b/spec/telegram/bot/updates_controller/reply_helpers_spec.rb @@ -4,8 +4,8 @@ RSpec.describe Telegram::Bot::UpdatesController do let(:respond_type) { :photo } let(:result) { double(:result) } let(:payload_type) { :message } - let(:payload) { {message_id: double(:message_id)} } - let(:chat) { {'id' => double(:chat_id)} } + let(:payload) { {message_id: double(:message_id), chat: chat} } + let(:chat) { {id: double(:chat_id)} } shared_examples 'missing chat' do context 'when chat is missing' do @@ -19,9 +19,8 @@ RSpec.describe Telegram::Bot::UpdatesController do include_examples 'missing chat' it 'sets chat_id & reply_to_message_id' do - expect(controller).to receive(:chat) { chat } expect(bot).to receive("send_#{respond_type}"). - with(params.merge(chat_id: chat['id'])) { result } + with(params.merge(chat_id: chat[:id])) { result } should eq result end end @@ -31,19 +30,18 @@ RSpec.describe Telegram::Bot::UpdatesController do include_examples 'missing chat' it 'sets chat_id & reply_to_message_id' do - expect(controller).to receive(:chat) { chat } expect(bot).to receive("send_#{respond_type}").with(params.merge( - chat_id: chat['id'], + chat_id: chat[:id], reply_to_message_id: payload[:message_id], )) { result } should eq result end context 'when update is not set' do - let(:update) { {chat: chat} } + let(:controller_args) { [bot, chat: deep_stringify(chat)] } it 'sets chat_id' do expect(bot).to receive("send_#{respond_type}"). - with(params.merge(chat_id: chat['id'])) { result } + with(params.merge(chat_id: chat[:id])) { result } should eq result end end @@ -70,7 +68,7 @@ RSpec.describe Telegram::Bot::UpdatesController do it 'sets chat_id & message_id' do expect(bot).to receive("edit_message_#{type}").with(params.merge( message_id: payload[:message][:message_id], - chat_id: payload[:message][:chat]['id'], + chat_id: payload[:message][:chat][:id], )) { result } should eq result end diff --git a/spec/telegram/bot/updates_controller/session_spec.rb b/spec/telegram/bot/updates_controller/session_spec.rb index 103fd56..97406fb 100644 --- a/spec/telegram/bot/updates_controller/session_spec.rb +++ b/spec/telegram/bot/updates_controller/session_spec.rb @@ -36,7 +36,7 @@ RSpec.describe Telegram::Bot::UpdatesController::Session do end def build_message(text, from) - {'message' => {'text' => text, 'from' => from.stringify_keys}} + deep_stringify(message: {text: text, from: from}) end it 'stores session between requests' do diff --git a/spec/telegram/bot/updates_controller_spec.rb b/spec/telegram/bot/updates_controller_spec.rb index 4389dfb..93550ad 100644 --- a/spec/telegram/bot/updates_controller_spec.rb +++ b/spec/telegram/bot/updates_controller_spec.rb @@ -278,11 +278,13 @@ RSpec.describe Telegram::Bot::UpdatesController do instance_eval(&block) context 'when re-initialized' do let(:controller) do - described_class.new(double(:other_bot), build_update(:message, + initial_update = deep_stringify message: { text: 'original message', from: double(:original_from), chat: double(:original_chat), - )).tap { |x| x.send(:initialize, bot, update) } + } + described_class.new(double(:other_bot), initial_update). + tap { |x| x.send(:initialize, *controller_args) } end instance_eval(&block) end @@ -300,7 +302,7 @@ RSpec.describe Telegram::Bot::UpdatesController do end context 'when options hash is given' do - let(:update) { {from: from, chat: chat} } + let(:controller_args) { [bot, from: from, chat: chat] } with_reinitialize do its(:bot) { should eq bot } its(:update) { should eq nil } From 050df20533778130b50d9011e8b53542cd0e1424 Mon Sep 17 00:00:00 2001 From: Max Melentiev Date: Fri, 25 May 2018 16:35:23 +0300 Subject: [PATCH 3/7] Require action_dispatch fully It uses autoload, so it's lazy-loaded. However there are missing some internal requires so it will asks for more not-related `requires` when requiring some files directly. --- lib/telegram/bot/middleware.rb | 3 +-- spec/integration_helper.rb | 1 - spec/telegram/bot/middleware_spec.rb | 1 - spec/telegram/bot/rspec/integration_spec.rb | 1 - 4 files changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/telegram/bot/middleware.rb b/lib/telegram/bot/middleware.rb index 70bbd30..b8931f3 100644 --- a/lib/telegram/bot/middleware.rb +++ b/lib/telegram/bot/middleware.rb @@ -1,8 +1,7 @@ require 'active_support/concern' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/json' -require 'action_dispatch/http/mime_type' -require 'action_dispatch/http/request' +require 'action_dispatch' module Telegram module Bot diff --git a/spec/integration_helper.rb b/spec/integration_helper.rb index 941d11e..648d835 100644 --- a/spec/integration_helper.rb +++ b/spec/integration_helper.rb @@ -1,7 +1,6 @@ require 'telegram/bot/rspec/integration' require 'action_controller' require 'action_dispatch' -require 'action_dispatch/testing/integration' require 'rails' require 'telegram/bot/railtie' diff --git a/spec/telegram/bot/middleware_spec.rb b/spec/telegram/bot/middleware_spec.rb index e5d48b4..7d84f51 100644 --- a/spec/telegram/bot/middleware_spec.rb +++ b/spec/telegram/bot/middleware_spec.rb @@ -20,7 +20,6 @@ RSpec.describe Telegram::Bot::Middleware do if ActionPack::VERSION::MAJOR < 5 # Before Rails 5, params are parsed in middleware. # In Rails 5, they are parsed in Request#request_parameters. - require 'action_dispatch/middleware/params_parser' let(:instance) { ActionDispatch::ParamsParser.new(super()) } end diff --git a/spec/telegram/bot/rspec/integration_spec.rb b/spec/telegram/bot/rspec/integration_spec.rb index 070947a..758fee5 100644 --- a/spec/telegram/bot/rspec/integration_spec.rb +++ b/spec/telegram/bot/rspec/integration_spec.rb @@ -1,7 +1,6 @@ require 'telegram/bot/rspec/integration' require 'action_controller' require 'action_dispatch' -require 'action_dispatch/testing/integration' RSpec.describe 'Integrations helper', :telegram_bot do include ActionDispatch::Integration::Runner From 4d76941d0331aea00c394384fa926d90e2c4bbea Mon Sep 17 00:00:00 2001 From: Max Melentiev Date: Fri, 25 May 2018 18:48:38 +0300 Subject: [PATCH 4/7] 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 From ad798cd48dc25aab7d6db1658cc6ac9f2d44ca79 Mon Sep 17 00:00:00 2001 From: Daniel Pietersen Date: Tue, 5 Jun 2018 17:05:59 +0200 Subject: [PATCH 5/7] Testing Cleanup Thought I'd suggest some edits based on grammar and some spelling mistakes considering how useful this Bot has been for the project I'm working on. Feel free to ignore if it's not needed. --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 11d1ee3..f2bb69f 100644 --- a/README.md +++ b/README.md @@ -395,11 +395,11 @@ Telegram::Bot::UpdatesPoller.start(bot, controller_class) ### Testing -There is `Telegram::Bot::ClientStub` class to stub client for tests. -Instead of performing API requests it stores them in `requests` hash. +There is a `Telegram::Bot::ClientStub` class to stub client for tests. +Instead of performing API requests it stores them in a `requests` hash. To stub all possible clients use `Telegram::Bot::ClientStub.stub_all!` before -initializing clients. Here is template for RSpec: +initializing clients. Here is a template for RSpec: ```ruby # environments/test.rb @@ -418,23 +418,23 @@ end ``` RSpec contexts and helpers are included automatically for groups and examples with matching -tags. In RSpec < 3.4 it's require to use `include_context` explicitly. +tags. In RSpec < 3.4 you're required 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. There are 3 types of integration tests: -- `:rails` - for testing bot in webhooks-mode in Rails application. +- `: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. +- `:rack` - For testing bot in webhooks-mode in non-Rails application. + It uses the `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: +Here is an example test for a Rails app: ```ruby # spec/requests/telegram_webhooks_spec.rb @@ -453,7 +453,7 @@ RSpec.describe TelegramWebhooksController, telegram_bot: :rails do # The most basic one is #make_telegram_request(bot, endpoint, params_matcher) expect { dispatch_command(:start) }. to make_telegram_request(bot, :sendMessage, hash_including(text: 'msg text')) - # There are some shortcuts for ba + # There are some shortcuts for basic commands and responses. expect { send_message('Hi') }.to send_telegram_message(bot, /msg regexp/, some: :option) end @@ -474,12 +474,12 @@ RSpec.describe TelegramWebhooksController, telegram_bot: :rails do 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. Among the differencies is -that controller tests use single controller instance for all dispatches in single exaple, +There is a context for testing bot controller in the way similar to Rails controller tests. +It's supposed to be a low-level alternative for request specs. Among the differencies is +that controller tests use a single controller instance for all dispatches in a specific example, 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. +so it also supports mocks. This can be useful for unit testing, but should not be used as +the default way to test the bot. ```ruby require 'telegram/bot/updates_controller/rspec_helpers' From a9b45c70cf742f0c107b193965172cbb7d996cb4 Mon Sep 17 00:00:00 2001 From: Max Melentiev Date: Wed, 6 Jun 2018 01:47:17 +0600 Subject: [PATCH 6/7] Update readme --- README.md | 29 ++++++++++++--------- lib/telegram/bot/rspec/integration/rails.rb | 1 - 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f2bb69f..9c6c9de 100644 --- a/README.md +++ b/README.md @@ -418,21 +418,23 @@ end ``` RSpec contexts and helpers are included automatically for groups and examples with matching -tags. In RSpec < 3.4 you're required to use `include_context` explicitly. +tags. In RSpec < 3.4 it's required 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. 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. +- `:rails` - for testing bot in webhooks-mode in Rails application. + It simulates webhook requests POSTing data to controller's endpoint. + It works on the top of requests specs, so `rspec-rails` gem is required. - `:rack` - For testing bot in webhooks-mode in non-Rails application. - It uses the `rack-test` gem to POST requests to bot's endpoint. + 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/`. +for each type in +[telegram/bot/rspec/integration/](https://github.com/telegram-bot-rb/telegram-bot/tree/master/lib/telegram/bot/rspec/integration). Here is an example test for a Rails app: @@ -445,20 +447,21 @@ RSpec.describe TelegramWebhooksController, telegram_bot: :rails do # include_context 'telegram/bot/integration/rails' # Main method is #dispatch(update). Some helpers are: - # #dispatch_message(text, options = {}) - # #dispatch_command(cmd, *args) + # dispatch_message(text, options = {}) + # dispatch_command(cmd, *args) # Available matchers can be found in Telegram::Bot::RSpec::ClientMatchers. it 'shows usage of basic matchers' # The most basic one is #make_telegram_request(bot, endpoint, params_matcher) expect { dispatch_command(:start) }. to make_telegram_request(bot, :sendMessage, hash_including(text: 'msg text')) - # There are some shortcuts for basic commands and responses. - expect { send_message('Hi') }.to send_telegram_message(bot, /msg regexp/, some: :option) + + # There are some shortcuts for dispatching basic updates and testing responses. + expect { dispatch_message('Hi') }.to send_telegram_message(bot, /msg regexp/, some: :option) end describe '#start' do - subject { -> { dispatch_command :start } }sic actions. + subject { -> { dispatch_command :start } } # Using built in matcher for `respond_to`: it { should respond_with_message 'Hi there!' } end @@ -475,8 +478,8 @@ end ``` There is a context for testing bot controller in the way similar to Rails controller tests. -It's supposed to be a low-level alternative for request specs. Among the differencies is -that controller tests use a single controller instance for all dispatches in a specific example, +It's supposed to be a low-level alternative for integration tests. Among the differences is +that controller tests use a single controller instance for all dispatches in specific exaple, session is stubbed (does not use configured store engine), and update is not serialized so it also supports mocks. This can be useful for unit testing, but should not be used as the default way to test the bot. @@ -486,6 +489,8 @@ require 'telegram/bot/updates_controller/rspec_helpers' RSpec.describe TelegramWebhooksController, type: :telegram_bot_controller do # for old RSpec: # include_context 'telegram/bot/updates_controller' + + # Same helpers and matchers like dispatch_command, answer_callback_query are available here. end ``` diff --git a/lib/telegram/bot/rspec/integration/rails.rb b/lib/telegram/bot/rspec/integration/rails.rb index d51c97d..aa7502e 100644 --- a/lib/telegram/bot/rspec/integration/rails.rb +++ b/lib/telegram/bot/rspec/integration/rails.rb @@ -1,6 +1,5 @@ require 'telegram/bot/rspec/integration/shared' -# RSpec.shared_context 'telegram/bot/integration/rails', type: :request do include_context 'telegram/bot/integration/shared' From fb02c34be938986189dc4dc9e75db8c911004746 Mon Sep 17 00:00:00 2001 From: Max Melentiev Date: Wed, 6 Jun 2018 13:33:32 +0600 Subject: [PATCH 7/7] Add deprecated fallback at telegram/bot/rspec/integration --- CHANGELOG.md | 3 ++- lib/telegram/bot/rspec/integration.rb | 10 ++++++++++ lib/telegram/bot/rspec/integration/rails.rb | 1 - spec/integration/requests/default_bot_spec.rb | 2 +- spec/integration/requests/named_bot_spec.rb | 2 +- spec/integration/requests/other_bot_spec.rb | 2 +- spec/spec_helper.rb | 8 ++++++++ 7 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 lib/telegram/bot/rspec/integration.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 52ac692..ce712c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ - `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`. + - Requiring `telegram/bot/rspec/integration` is deprecated in favor of + `telegram/bot/rspec/integration/rails`. - `:telegram_bot` rspec tag is replaced with `telegram_bot: :rails`. # 0.13.1 diff --git a/lib/telegram/bot/rspec/integration.rb b/lib/telegram/bot/rspec/integration.rb new file mode 100644 index 0000000..e28ca80 --- /dev/null +++ b/lib/telegram/bot/rspec/integration.rb @@ -0,0 +1,10 @@ +require 'telegram/bot' +Telegram::Bot.deprecation_0_15.warn( + "`require 'telegram/bot/rspec/integration'` is deprecated in favor of " \ + "`require 'telegram/bot/rspec/integration/rails'`" +) +require 'telegram/bot/rspec/integration/rails' + +Telegram::Bot::RSpec.with_include_context do |config| + config.include_context 'telegram/bot/integration/rails', telegram_bot: true +end diff --git a/lib/telegram/bot/rspec/integration/rails.rb b/lib/telegram/bot/rspec/integration/rails.rb index aa7502e..1e43e09 100644 --- a/lib/telegram/bot/rspec/integration/rails.rb +++ b/lib/telegram/bot/rspec/integration/rails.rb @@ -24,6 +24,5 @@ RSpec.shared_context 'telegram/bot/integration/rails', type: :request do 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/spec/integration/requests/default_bot_spec.rb b/spec/integration/requests/default_bot_spec.rb index 3791341..81a442b 100644 --- a/spec/integration/requests/default_bot_spec.rb +++ b/spec/integration/requests/default_bot_spec.rb @@ -1,6 +1,6 @@ require 'integration_helper' -RSpec.describe DefaultBotController, :telegram_bot, type: :request do +RSpec.describe DefaultBotController, telegram_bot: :rails do describe '#start' do subject { -> { dispatch_command :start } } it { should respond_with_message 'from default' } diff --git a/spec/integration/requests/named_bot_spec.rb b/spec/integration/requests/named_bot_spec.rb index 11e2eb2..cccfbd4 100644 --- a/spec/integration/requests/named_bot_spec.rb +++ b/spec/integration/requests/named_bot_spec.rb @@ -1,6 +1,6 @@ require 'integration_helper' -RSpec.describe NamedBotController, :telegram_bot, type: :request do +RSpec.describe NamedBotController, telegram_bot: :rails do let(:bot) { Telegram.bots[:named] } describe '#start' do subject { -> { dispatch_command :start } } diff --git a/spec/integration/requests/other_bot_spec.rb b/spec/integration/requests/other_bot_spec.rb index ddcdb90..8ccb783 100644 --- a/spec/integration/requests/other_bot_spec.rb +++ b/spec/integration/requests/other_bot_spec.rb @@ -1,6 +1,6 @@ require 'integration_helper' -RSpec.describe OtherBotController, :telegram_bot, type: :request do +RSpec.describe OtherBotController, telegram_bot: :rails do let(:bot) { Telegram.bots[:other] } describe '#start' do subject { -> { dispatch_command :start } } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 54ce8e2..1be5648 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -32,6 +32,14 @@ RSpec.configure do |config| expectations.include_chain_clauses_in_custom_matcher_descriptions = true end + # This config option will be enabled by default on RSpec 4, + # but for reasons of backwards compatibility, you have to + # set it on RSpec 3. + # + # It causes the host group and examples to inherit metadata + # from the shared context. + config.shared_context_metadata_behavior = :apply_to_host_groups + config.mock_with :rspec do |mocks| # Prevents you from mocking or stubbing a method that does not exist on # a real object. This is generally recommended, and will default to