1
0
зеркало из https://github.com/glebtv/telegram-bot.git synced 2026-08-28 15:26:18 +03:00

Support for testing bots in poller mode and non-Rails apps

Этот коммит содержится в:
Max Melentiev
2018-05-25 18:48:38 +03:00
родитель 050df20533
Коммит 4d76941d03
18 изменённых файлов: 331 добавлений и 170 удалений

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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'

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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

24
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

30
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

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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

Просмотреть файл

@@ -1,4 +1,4 @@
require 'telegram/bot/rspec/integration'
require 'telegram/bot/rspec/integration/rails'
require 'action_controller'
require 'action_dispatch'

24
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

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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

Просмотреть файл

@@ -0,0 +1 @@
# Tested in spec/integration

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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