Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improving Testability of Handlers v2 #111

Merged
merged 34 commits into from
Apr 11, 2020
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
6f95895
Refactor Outbound Slack Connection into a behavior abstraction
NateBarnes Apr 8, 2020
db0b8e2
Refactored internal tests to use Mox library and added tests for ping…
NateBarnes Apr 8, 2020
eea8685
Extract conn creation into a test helper
NateBarnes Apr 8, 2020
83e7e20
Rename slack_api to more generic outbound_api
NateBarnes Apr 8, 2020
1f99111
Refactored mocking infrastructure into a Case class with associated m…
NateBarnes Apr 8, 2020
84b1d78
Extracted more test context into helpers to make testing easier
NateBarnes Apr 8, 2020
002ec1c
Added testing instructions to the README
NateBarnes Apr 9, 2020
38166cf
Properly mark the test_helper section of the README as elixir
NateBarnes Apr 9, 2020
fc76e6e
Changed from a mock to a spy
NateBarnes Apr 10, 2020
1da74be
Renamed Spy correctly
NateBarnes Apr 10, 2020
e317d4a
Removed mox dependency
NateBarnes Apr 10, 2020
7973085
Make sure test supports are in the path
NateBarnes Apr 10, 2020
371453f
Update README to match new testing style
NateBarnes Apr 10, 2020
6bba99f
Implemented new test_message method to actually test the routing
NateBarnes Apr 10, 2020
1e8486b
Renamed Case helper methods
NateBarnes Apr 10, 2020
f79724a
Use better format for Enum checking
NateBarnes Apr 10, 2020
1a7a23d
Fill out more of the fke connection
NateBarnes Apr 10, 2020
c4da8d8
Remove logger from testing
NateBarnes Apr 11, 2020
8876d6e
Update README
NateBarnes Apr 11, 2020
1d405be
Unify to one fake_conn function
NateBarnes Apr 11, 2020
e30d46e
Update README to include the command string
NateBarnes Apr 11, 2020
58e04a1
Add tests for typing
NateBarnes Apr 11, 2020
1fa9ab3
Update test/alice/router/helpers_test.exs
NateBarnes Apr 11, 2020
0b934f5
Use an atom instead of nil
NateBarnes Apr 11, 2020
d79743a
Better structure on fake_conn_with_capture
NateBarnes Apr 11, 2020
80e8dc1
Removed mox from lockfile
NateBarnes Apr 11, 2020
59650ab
Rename Alice.Handlers.Case to Alice.HandlersCase
NateBarnes Apr 11, 2020
fb74d21
Automatically add ExUnit.Case
NateBarnes Apr 11, 2020
923b358
Remove need for ExUnit in Handler tests
NateBarnes Apr 11, 2020
a31ba61
Rename receive_message to send_message
NateBarnes Apr 11, 2020
61aed73
Split send_message funciton to allow using with a connection
NateBarnes Apr 11, 2020
72e01a5
reworked fake_conn_with_capture
NateBarnes Apr 11, 2020
17753bd
ALL THE DOCUMENTATION
NateBarnes Apr 11, 2020
350e448
Formatting changes
NateBarnes Apr 11, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,33 @@ defmodule Alice.Handlers.GoogleImages do
end
```

### Testing Handlers

Alice provides several helpers to make it easy to test your handlers.
First you'll need to invoke to add `use Alice.HandlersCase, handlers:
[YourHandler]` passing it the handler you're trying to test. Then you
can use `message_received()` within your test, which will simulate a
message coming in from the chat backend and route it through to the
handlers appropriately. If you're wanting to invoke a command, you'll
need to make sure your message includes `<@alice>` within the string. From there you can use either `first_reply()`
to get the first reply sent out or `all_replies()` which will return a List of replies that have been
received during your test. You can use either to use normal assertions
on to ensure your handler behaves in the manner you expect.

In `test/alice/handlers/google_images_test.exs`:

```elixir
defmodule Alice.Handlers.GoogleImagesTest do
use Alice.HandlersCase, handlers: Alice.Handlers.GoogleImages

test "it fetches an image when asked" do
send_message("img me example image")

assert first_reply() == "http://example.com/image_from_google.jpg"
end
end
```

### Registering Handlers

In the `mix.exs` file of your bot, add your handler to the list of handlers to
Expand Down
8 changes: 8 additions & 0 deletions lib/alice/chat_backends/outbound_client.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule Alice.ChatBackends.OutboundClient do
@moduledoc """
Documentation for the OutboundClient behavior. This defines a behavior for modules that serve as an outbound connection to a backend.
"""

@callback send_message(response :: String.t(), channel :: String.t(), backend :: map()) :: String.t()
@callback indicate_typing(channel :: String.t(), backend :: map()) :: String.t()
end
7 changes: 7 additions & 0 deletions lib/alice/chat_backends/slack_outbound.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule Alice.ChatBackends.SlackOutbound do
@behaviour Alice.ChatBackends.OutboundClient

def send_message(response, channel, slack), do: Slack.Sends.send_message(response, channel, slack)

def indicate_typing(channel, slack), do: Slack.Sends.indicate_typing(channel, slack)
end
4 changes: 3 additions & 1 deletion lib/alice/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ defmodule Alice.Router do

def match_pattern({pattern, name}, {mod, conn = %Conn{}}) do
if Regex.match?(pattern, conn.message.text) do
Logger.info("#{mod}.#{name} responding to -> #{Conn.user(conn)}")
unless(Mix.env() == :test) do
Logger.info("#{mod}.#{name} responding to -> #{Conn.user(conn)}")
end
{mod, apply(mod, name, [Conn.add_captures(conn, pattern)])}
else
{mod, conn}
Expand Down
11 changes: 5 additions & 6 deletions lib/alice/router/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ defmodule Alice.Router.Helpers do
def reply(conn = %Conn{message: %{channel: channel}, slack: slack}, resp) do
resp
|> Alice.Images.uncache()
|> slack_api().send_message(channel, slack)
|> outbound_api().send_message(channel, slack)

conn
end

defp slack_api do
defp outbound_api do
case Mix.env() do
:test -> FakeSlack
_else -> Slack.Sends
:test -> Alice.ChatBackends.OutboundSpy
_else -> Alice.ChatBackends.SlackOutbound
end
end

Expand Down Expand Up @@ -89,7 +89,6 @@ defmodule Alice.Router.Helpers do
@spec delayed_reply(Conn.t(), String.t(), integer) :: Task.t()
@spec delayed_reply(String.t(), integer, Conn.t()) :: Task.t()
def delayed_reply(msg, ms, conn = %Conn{}), do: delayed_reply(conn, msg, ms)

def delayed_reply(conn = %Conn{}, message, milliseconds) do
Task.async(fn ->
conn = indicate_typing(conn)
Expand All @@ -103,7 +102,7 @@ defmodule Alice.Router.Helpers do
"""
@spec indicate_typing(Conn.t()) :: Conn.t()
def indicate_typing(conn = %Conn{message: %{channel: chan}, slack: slack}) do
slack_api().indicate_typing(chan, slack)
outbound_api().indicate_typing(chan, slack)
conn
end
end
3 changes: 3 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ defmodule Alice.Mixfile do
app: :alice,
version: "0.4.1",
elixir: "~> 1.7",
elixirc_paths: elixirc_paths(Mix.env()),
build_embedded: Mix.env() == :prod,
start_permanent: Mix.env() == :prod,
description: "A Slack bot",
Expand Down Expand Up @@ -43,4 +44,6 @@ defmodule Alice.Mixfile do
}
]
end

defp elixirc_paths(_), do: ["test/support", "lib"]
end
16 changes: 16 additions & 0 deletions test/alice/handlers/utils_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
defmodule Alice.Handlers.UtilsTest do
use Alice.HandlersCase, handlers: Alice.Handlers.Utils

test "it should respond to a ping" do
send_message("ping")

assert first_reply() in ["PONG!", "Can I help you?", "Yes...I'm still here.", "I'm alive!"]
end

test "it should respond with info about the running bot" do
send_message("<@alice> info")

{:ok, version} = :application.get_key(:alice, :vsn)
assert first_reply() == "Alice #{version} - https://github.com/alice-bot"
end
end
60 changes: 34 additions & 26 deletions test/alice/router/helpers_test.exs
Original file line number Diff line number Diff line change
@@ -1,55 +1,63 @@
defmodule FakeSlack do
def send_message(text, :channel, :slack) do
send(self(), {:msg, text})
end
end

defmodule Alice.Router.HelpersTest do
use ExUnit.Case, async: true
use ExUnit.Case
import Alice.HandlersCase
import Alice.Router.Helpers

def conn do
%Alice.Conn{message: %{channel: :channel}, slack: :slack}
end

test "reply returns the conn" do
assert reply("yo", conn()) == conn()
assert reply("yo", fake_conn()) == fake_conn()
end

test "reply sends a message with Slack.send_message" do
reply("yo", conn())
assert_received {:msg, "yo"}
reply("yo", fake_conn())

assert first_reply() == "yo"
end

test "multiple replies can be sent in the same handler" do
reply("first", fake_conn())
reply("second", fake_conn())

assert ["first", "second"] == all_replies()
end

test "reply calls random_reply when given a list" do
["element"] |> reply(conn())
assert_received {:msg, "element"}
reply(["element"], fake_conn())

assert first_reply() == "element"
end

test "random_reply sends a message from a given list" do
~w[rabbit hole] |> random_reply(conn())
assert_received {:msg, resp}
assert resp in ~w[rabbit hole]
~w[rabbit hole] |> random_reply(fake_conn())

assert first_reply() in ~w[rabbit hole]
end

test "chance_reply, when chance passes, \
replies with the given message" do
chance_reply(conn(), 1, "always")
assert_received {:msg, "always"}
chance_reply(fake_conn(), 1, "always")

assert first_reply() == "always"
end

test "chance_reply, when chance does not pass, \
when not given negative message, \
does not reply" do
chance_reply(conn(), 0, "never")
refute_received {:msg, _}
chance_reply(fake_conn(), 0, "never")

assert first_reply() == nil
end

test "chance_reply, when chance does not pass, \
when given negative message, \
replies with negative" do
chance_reply(conn(), 0, "positive", "negative")
refute_received {:msg, "positive"}
assert_received {:msg, "negative"}
chance_reply(fake_conn(), 0, "positive", "negative")

assert all_replies() == ["negative"]
end

test "it should indicate typing when asked" do
indicate_typing(fake_conn())

assert typing?()
end
end
68 changes: 68 additions & 0 deletions test/support/handlers_case.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
defmodule Alice.HandlersCase do
def all_replies() do
message = receive do
{:send_message, %{response: message}} -> message
after
0 -> :no_message_received
end
case message do
:no_message_received -> []
message -> [message | all_replies()]
end
end

def first_reply() do
case all_replies() do
[first_message | _] -> first_message
_ -> nil
end
end

def fake_conn(), do: fake_conn("")
def fake_conn(text) do
%Alice.Conn{message: %{text: text, channel: :channel, user: :fake_user}, slack: %{users: [fake_user: %{name: "fake_user"}], me: %{id: :alice}}}
end
def fake_conn(message, capture: capture_regex) do
message
|> fake_conn()
|> Alice.Conn.add_captures(capture_regex)
end


def send_message(conn = %Alice.Conn{}) do
case Alice.Conn.command?(conn) do
true -> Alice.Router.match_commands(conn)
false -> Alice.Router.match_routes(conn)
end
end
def send_message(message) do
message
|> fake_conn()
|> send_message()
end

def typing?() do
receive do
{:indicate_typing, _} -> true
after
0 -> false
end
end

defmacro __using__(opts \\ []) do
handlers = opts
|> Keyword.get(:handlers, [])
|> List.wrap()

quote do
use ExUnit.Case
import Alice.HandlersCase

setup do
Alice.Router.start_link(unquote(handlers))

:ok
end
end
end
end
11 changes: 11 additions & 0 deletions test/support/outbound_fake.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defmodule Alice.ChatBackends.OutboundSpy do
@behaviour Alice.ChatBackends.OutboundClient

def send_message(response, channel, slack) do
send(self(), {:send_message, %{response: response, channel: channel, slack: slack}})
end

def indicate_typing(channel, slack) do
send(self(), {:indicate_typing, %{channel: channel, slack: slack}})
end
end
18 changes: 0 additions & 18 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -1,19 +1 @@
defmodule Mock do
def setup(name, params), do: setup(name, params, default_return: nil)

def setup(name, params, default_return: value) do
send(self(), {name, params})

receive do
{:return, {^name, value}} -> value
after
0 -> value
end
end

def setup_return(name, value) do
send(self(), {:return, {name, value}})
end
end

ExUnit.start()