From 673986ca7ef5c8e2ef27cbbccbd1b2e88e9d2d6d Mon Sep 17 00:00:00 2001 From: Max Melentiev Date: Mon, 21 May 2018 12:19:44 +0300 Subject: [PATCH] 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 }