diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 4408052..a7b33a4 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2018-08-27 14:16:34 +0200 using RuboCop version 0.58.2. +# on 2019-03-25 10:27:43 -0400 using RuboCop version 0.58.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new diff --git a/CHANGELOG.md b/CHANGELOG.md index 49ca88f..d4b373f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ ### 0.12.1 (Next) -* Your contribution here. +* [#209](https://github.com/slack-ruby/slack-ruby-bot/pull/209): Allow `respond_to_slack_message` and `respond_to_slack_messages` without arguments - [@dblock](https://github.com/dblock). +* [#216](https://github.com/slack-ruby/slack-ruby-bot/pull/216): Added `start_typing` RSpec matcher - [@dblock](https://github.com/dblock). * [#214](https://github.com/slack-ruby/slack-ruby-bot/pull/214): Add passenger deployment documentation - [@cybercrediators](https://github.com/cybercrediators). +* Your contribution here. ### 0.12.0 (2019/2/25) diff --git a/README.md b/README.md index 4ccaee5..313d2dd 100644 --- a/README.md +++ b/README.md @@ -382,7 +382,7 @@ Get help. Hooks are event handlers and respond to Slack RTM API [events](https://api.slack.com/events), such as [hello](lib/slack-ruby-bot/hooks/hello.rb) or [message](lib/slack-ruby-bot/hooks/message.rb). You can implement your own in a couple of ways: -#### Implement and register a Hook Handler +#### Implementing and registering a Hook Handler A Hook Handler is any object that respond to a `call` message, like a proc, instance of an object, class with a `call` class method, etc. @@ -653,14 +653,17 @@ Again, the View will have access to the most up to date `client`, `data`, and `m View methods are not matched to routes, so there is no restriction on how to name methods as there is in Controllers. -### RSpec Shared Behaviors +### Testing -Slack-ruby-bot ships with a number of shared RSpec behaviors that can be used in your RSpec tests. +#### RSpec Shared Behaviors + +Slack-ruby-bot comes with a number of shared RSpec behaviors that can be used in your RSpec tests. * [behaves like a slack bot](lib/slack-ruby-bot/rspec/support/slack-ruby-bot/it_behaves_like_a_slack_bot.rb): A bot quacks like a Slack Ruby bot. * [respond with slack message](lib/slack-ruby-bot/rspec/support/slack-ruby-bot/respond_with_slack_message.rb): The bot responds with a message. * [respond with slack messages](lib/slack-ruby-bot/rspec/support/slack-ruby-bot/respond_with_slack_messages.rb): The bot responds with a multiple messages. * [respond with error](lib/slack-ruby-bot/rspec/support/slack-ruby-bot/respond_with_error.rb): An exception is raised inside a bot command. +* [start typing](lib/slack-ruby-bot/rspec/support/slack-ruby-bot/start_typing.rb): The bot calls `client.start_typing`. Require `slack-ruby-bot/rspec` in your `spec_helper.rb` along with the following dependencies in Gemfile. @@ -673,6 +676,72 @@ group :development, :test do end ``` +Use the `respond_with_slack_message` matcher. + +```ruby +describe SlackRubyBot::Commands do + it 'responds with any message' do + expect(message: "#{SlackRubyBot.config.user} hi").to respond_with_slack_message + end + it 'says hi' do + expect(message: "#{SlackRubyBot.config.user} hi").to respond_with_slack_message('hi') + end +end +``` + +Use the `respond_with_slack_messages` matcher for multiple messages. + +```ruby +describe SlackRubyBot::Commands do + it 'responds with more than one message' do + expect(message: "#{SlackRubyBot.config.user} count").to respond_with_slack_messages + end + it 'says one and two' do + expect(message: "#{SlackRubyBot.config.user} count").to respond_with_slack_messages(['one', 'two']) + end +end +``` + +Message matchers support regular expressions. + +```ruby +describe SlackRubyBot::Commands do + it 'says hi' do + expect(message: "#{SlackRubyBot.config.user} hi").to respond_with_slack_message(/hi/) + end +end +``` + +Check that the bot called `client.start_typing(channel: 'channel')`. + +```ruby +describe SlackRubyBot::Commands do + it 'starts typing on channel' do + expect(message: "#{SlackRubyBot.config.user} hi").to start_typing(channel: 'channel') + end +end +``` + +#### Testing Lower Level Messages + +You can test client behavior at a lower level by fetching the message hook. The following example expects a bot command to call `client.typing(channel: data.channel)`. + +```ruby +describe SlackRubyBot::Commands do + let(:app) { Server.new } + let(:client) { app.send(:client) } + let(:message_hook) { SlackRubyBot::Hooks::Message.new } + it 'receives a typing event' do + expect(client).to receive(:typing) + message_hook.call( + client, + Hashie::Mash.new(text: "#{SlackRubyBot.config.user} type something", channel: 'channel') + ) + end + end +end +``` + ### Useful Libraries * [newrelic-slack-ruby-bot](https://github.com/dblock/newrelic-slack-ruby-bot): NewRelic instrumentation for slack-ruby-bot. diff --git a/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/not_respond.rb b/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/not_respond.rb index f5cf7a0..c0a04eb 100644 --- a/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/not_respond.rb +++ b/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/not_respond.rb @@ -2,11 +2,7 @@ RSpec::Matchers.define :not_respond do match do |actual| - client = if respond_to?(:client) - send(:client) - else - SlackRubyBot::Client.new - end + client = respond_to?(:client) ? send(:client) : SlackRubyBot::Client.new message_command = SlackRubyBot::Hooks::Message.new channel, user, message, attachments = parse(actual) diff --git a/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/respond_with_error.rb b/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/respond_with_error.rb index e1eb235..962376b 100644 --- a/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/respond_with_error.rb +++ b/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/respond_with_error.rb @@ -2,11 +2,7 @@ RSpec::Matchers.define :respond_with_error do |error, error_message| match do |actual| - client = if respond_to?(:client) - send(:client) - else - SlackRubyBot::Client.new - end + client = respond_to?(:client) ? send(:client) : SlackRubyBot::Client.new message_command = SlackRubyBot::Hooks::Message.new channel, user, message, attachments = parse(actual) diff --git a/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/respond_with_slack_message.rb b/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/respond_with_slack_message.rb index 4fb0769..2d9d4f8 100644 --- a/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/respond_with_slack_message.rb +++ b/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/respond_with_slack_message.rb @@ -1,33 +1,34 @@ require 'rspec/expectations' + RSpec::Matchers.define :respond_with_slack_message do |expected| include SlackRubyBot::SpecHelpers + match do |actual| client = respond_to?(:client) ? send(:client) : SlackRubyBot::Client.new - def client.test_messages - @test_received_messages - end - - def client.say(options = {}) - super - @test_received_messages = @test_received_messages.nil? ? [] : @test_received_messages - @test_received_messages.push options - end message_command = SlackRubyBot::Hooks::Message.new channel, user, message, attachments = parse(actual) allow(Giphy).to receive(:random) if defined?(Giphy) - allow(client).to receive(:message) + allow(client).to receive(:message) do |options| + @messages ||= [] + @messages.push options + end + message_command.call(client, Hashie::Mash.new(text: message, channel: channel, user: user, attachments: attachments)) - @messages = client.test_messages - expect(client).to have_received(:message).with(hash_including(channel: channel, text: expected)).once + + matcher = have_received(:message).once + matcher = matcher.with(hash_including(channel: channel, text: expected)) if channel && expected + + expect(client).to matcher + true end failure_message do |_actual| message = "expected to receive message with text: #{expected} once,\n received:" - message += @messages.count.zero? ? 'No response messages received' : @messages.inspect + message += @messages && @messages.any? ? @messages.inspect : 'none' message end end diff --git a/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/respond_with_slack_messages.rb b/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/respond_with_slack_messages.rb index 010f64a..1298a38 100644 --- a/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/respond_with_slack_messages.rb +++ b/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/respond_with_slack_messages.rb @@ -1,38 +1,45 @@ require 'rspec/expectations' + RSpec::Matchers.define :respond_with_slack_messages do |expected| include SlackRubyBot::SpecHelpers + match do |actual| - raise ArgumentError, 'respond_with_slack_messages expects an array of ordered responses' unless expected.respond_to? :each - client = respond_to?(:client) ? send(:client) : SlackRubyBot::Client.new - def client.test_messages - @test_received_messages - end + raise ArgumentError, 'respond_with_slack_messages expects an array of ordered responses' if expected && !expected.respond_to?(:each) - def client.say(options = {}) - super - @test_received_messages = @test_received_messages.nil? ? [] : @test_received_messages - @test_received_messages.push options - end + client = respond_to?(:client) ? send(:client) : SlackRubyBot::Client.new message_command = SlackRubyBot::Hooks::Message.new channel, user, message, attachments = parse(actual) allow(Giphy).to receive(:random) if defined?(Giphy) - allow(client).to receive(:message) + @messages ||= [] + allow(client).to receive(:message) do |options| + @messages.push options + end + message_command.call(client, Hashie::Mash.new(text: message, channel: channel, user: user, attachments: attachments)) - @messages = client.test_messages + @responses = [] - expected.each do |exp| - @responses.push(expect(client).to(have_received(:message).with(hash_including(channel: channel, text: exp)).once)) + + if expected && expected.any? + expected.each do |exp| + @responses.push(expect(client).to(have_received(:message).with(hash_including(channel: channel, text: exp)).once)) + end + else + expect(@messages.size).to be > 1 end + true end + failure_message do |_actual| - message = '' - expected.each do |exp| - message += "Expected text: #{exp}, got #{@messages[expected.index(exp)] || 'No Response'}\n" unless @responses[expected.index(exp)] + if expected && expected.any? + expected.map do |exp| + "Expected text: #{exp}, got #{@messages[expected.index(exp)] || 'none'}" unless @responses[expected.index(exp)] + end.compact.join("\n") + else + "Expected to receive multiple messages, got #{@messages.any? ? @messages.size : 'none'}" end - message end end diff --git a/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/start_typing.rb b/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/start_typing.rb new file mode 100644 index 0000000..1296842 --- /dev/null +++ b/lib/slack-ruby-bot/rspec/support/slack-ruby-bot/start_typing.rb @@ -0,0 +1,30 @@ +require 'rspec/expectations' + +RSpec::Matchers.define :start_typing do |expected| + include SlackRubyBot::SpecHelpers + + match do |actual| + client = respond_to?(:client) ? send(:client) : SlackRubyBot::Client.new + + message_command = SlackRubyBot::Hooks::Message.new + + allow(client).to receive(:typing) do |options| + @test_options = options + end + + channel, user, message, attachments = parse(actual) + message_command.call(client, Hashie::Mash.new(text: message, channel: channel, user: user, attachments: attachments)) + + matcher = have_received(:typing).once + matcher = matcher.with(expected) if expected && expected.any? + expect(client).to matcher + + true + end + + failure_message do |_actual| + message = "expected to receive typing with: #{expected} once,\n received:" + message += @test_options && @test_options.any? ? @test_options.inspect : ' none' + message + end +end diff --git a/spec/slack-ruby-bot/rspec/respond_with_slack_message_spec.rb b/spec/slack-ruby-bot/rspec/respond_with_slack_message_spec.rb index b8a9d1a..881e1bc 100644 --- a/spec/slack-ruby-bot/rspec/respond_with_slack_message_spec.rb +++ b/spec/slack-ruby-bot/rspec/respond_with_slack_message_spec.rb @@ -1,6 +1,8 @@ describe RSpec do let! :command do Class.new(SlackRubyBot::Commands::Base) do + command 'no message' do |client, data, _match| + end command 'single message with as_user' do |client, data, _match| client.say(text: 'single response', channel: data.channel, as_user: true) end @@ -25,6 +27,22 @@ def app expect(message: "#{SlackRubyBot.config.user} single message") .to respond_with_slack_message(/single response/i) end + it 'respond with any message' do + expect(message: "#{SlackRubyBot.config.user} single message") + .to respond_with_slack_message + end + it 'correctly reports error' do + expect do + expect(message: "#{SlackRubyBot.config.user} single message") + .to respond_with_slack_message('another response') + end.to raise_error RSpec::Expectations::ExpectationNotMetError, "expected to receive message with text: another response once,\n received:[{:text=>\"single response\", :channel=>\"channel\"}]" + end + it 'correctly reports error without message' do + expect do + expect(message: "#{SlackRubyBot.config.user} no message") + .to respond_with_slack_message('another response') + end.to raise_error RSpec::Expectations::ExpectationNotMetError, "expected to receive message with text: another response once,\n received:none" + end it 'respond_with_single_message_using_partial_regex_match' do expect(message: "#{SlackRubyBot.config.user} single message") .to respond_with_slack_message(/si[n|N]gle/) diff --git a/spec/slack-ruby-bot/rspec/respond_with_slack_messages_spec.rb b/spec/slack-ruby-bot/rspec/respond_with_slack_messages_spec.rb index bf85a5c..a13d569 100644 --- a/spec/slack-ruby-bot/rspec/respond_with_slack_messages_spec.rb +++ b/spec/slack-ruby-bot/rspec/respond_with_slack_messages_spec.rb @@ -1,6 +1,11 @@ describe RSpec do let! :command do Class.new(SlackRubyBot::Commands::Base) do + command 'do not respond' do |client, data, _match| + end + command 'respond once' do |client, data, _match| + client.say(text: 'response', channel: data.channel, as_user: true) + end command 'respond with as_user' do |client, data, _match| 5.times do |i| client.say(text: "response #{i}", channel: data.channel, as_user: true) @@ -29,6 +34,26 @@ def app expect(message: "#{SlackRubyBot.config.user} respond 5 times") .to respond_with_slack_messages(expected_responses) end + it 'respond with multiple messages' do + expect(message: "#{SlackRubyBot.config.user} respond 5 times") + .to respond_with_slack_messages + end + it 'respond once' do + expect do + expect(message: "#{SlackRubyBot.config.user} respond once") + .to respond_with_slack_messages + end.to raise_error RSpec::Expectations::ExpectationNotMetError, 'Expected to receive multiple messages, got 1' + end + it 'do not respond' do + expect(message: "#{SlackRubyBot.config.user} do not respond") + .to_not respond_with_slack_messages + end + it 'correctly reports error on do not respond' do + expect do + expect(message: "#{SlackRubyBot.config.user} do not respond") + .to respond_with_slack_messages + end.to raise_error RSpec::Expectations::ExpectationNotMetError, 'Expected to receive multiple messages, got none' + end it 'respond_with_multiple_messages_using_string_matches' do expected_responses = [] 5.times { |i| expected_responses.push("response #{i}") } @@ -41,6 +66,15 @@ def app expect(message: "#{SlackRubyBot.config.user} respond 5 times") .not_to respond_with_slack_messages(expected_responses) end + it 'correctly reports error' do + expect do + expected_responses = [] + 6.times { |i| expected_responses.push("response #{i}") } + expect(message: "#{SlackRubyBot.config.user} respond 5 times") + .to respond_with_slack_messages(expected_responses) + end.to raise_error RSpec::Expectations::ExpectationNotMetError, 'Expected text: response 5, got none' + end + it 'respond_with_multiple_messages_using_string_matches_with_extra_arg' do expected_responses = [] 5.times { |i| expected_responses.push("response #{i}") } diff --git a/spec/slack-ruby-bot/rspec/start_typing_spec.rb b/spec/slack-ruby-bot/rspec/start_typing_spec.rb new file mode 100644 index 0000000..9a16f7d --- /dev/null +++ b/spec/slack-ruby-bot/rspec/start_typing_spec.rb @@ -0,0 +1,34 @@ +describe RSpec do + let! :command do + Class.new(SlackRubyBot::Commands::Base) do + command 'types on the correct channel' do |client, data, _match| + client.typing(channel: data.channel) + end + command 'types on another channel' do |client, _data, _match| + client.typing(channel: 'another') + end + end + end + + def app + SlackRubyBot::App.new + end + it 'types on any channel' do + expect(message: "#{SlackRubyBot.config.user} types on the correct channel", channel: 'channel') + .to start_typing + end + it 'types on another channel' do + expect(message: "#{SlackRubyBot.config.user} types on another channel", channel: 'channel') + .to start_typing(channel: 'another') + end + it 'types an invalid channel' do + expect(message: "#{SlackRubyBot.config.user} types on another channel", channel: 'channel') + .to_not start_typing(channel: 'invalid') + end + it 'correctly reports error' do + expect do + expect(message: "#{SlackRubyBot.config.user} types on another channel", channel: 'channel') + .to start_typing(channel: 'invalid') + end.to raise_error RSpec::Expectations::ExpectationNotMetError, "expected to receive typing with: {:channel=>\"invalid\"} once,\n received:{:channel=>\"another\"}" + end +end